mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
Merge branch 'main' of https://github.com/Ashutosh-Bhadauriya/formbricks into enhance/question-card-ui
This commit is contained in:
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -37,4 +37,5 @@ Fixes # (issue)
|
||||
- [ ] Checked for warnings, there are none
|
||||
- [ ] Removed all `console.logs`
|
||||
- [ ] Merged the latest changes from main onto my branch with `git pull origin main`
|
||||
- [ ] My changes don't cause any responsiveness issues
|
||||
- [ ] Updated the Formbricks Docs if changes were necessary
|
||||
|
||||
@@ -48,22 +48,6 @@ export default function TemplateList({ onTemplateClick, activeTemplate }: Templa
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onTemplateClick(activeTemplate);
|
||||
setActiveTemplate(activeTemplate);
|
||||
}}
|
||||
className={cn(
|
||||
activeTemplate?.name === customSurvey.name
|
||||
? "ring-brand border-transparent ring-2"
|
||||
: "hover:border-brand-dark border-dashed border-slate-300",
|
||||
"duration-120 group relative rounded-lg border-2 bg-transparent p-8 transition-colors duration-150"
|
||||
)}>
|
||||
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
|
||||
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 ">{customSurvey.name}</h3>
|
||||
<p className="text-left text-xs text-slate-600 ">{customSurvey.description}</p>
|
||||
</button> */}
|
||||
{templates
|
||||
.filter((template) => selectedFilter === ALL_CATEGORY_NAME || template.category === selectedFilter)
|
||||
.map((template: Template) => (
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { Template } from "@formbricks/types/templates";
|
||||
const thankYouCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Thank you!",
|
||||
subheader: "We appreciate your time and insight.",
|
||||
subheader: "We appreciate your feedback.",
|
||||
};
|
||||
|
||||
export const customSurvey: Template = {
|
||||
|
||||
@@ -78,7 +78,12 @@ export default function PreviewSurvey({
|
||||
frameRef.current = requestAnimationFrame(frame);
|
||||
} else {
|
||||
handleStopCountdown();
|
||||
// close modal
|
||||
setIsModalOpen(false);
|
||||
// reopen the modal after 1 second
|
||||
setTimeout(() => {
|
||||
setIsModalOpen(true);
|
||||
setActiveQuestionId(questions[0]?.id || ""); // set first question as active
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,13 +138,13 @@ export default function PreviewSurvey({
|
||||
case "notEquals":
|
||||
return answerValue !== logic.value;
|
||||
case "lessThan":
|
||||
return answerValue < logic.value;
|
||||
return logic.value !== undefined && answerValue < logic.value;
|
||||
case "lessEqual":
|
||||
return answerValue <= logic.value;
|
||||
return logic.value !== undefined && answerValue <= logic.value;
|
||||
case "greaterThan":
|
||||
return answerValue > logic.value;
|
||||
return logic.value !== undefined && answerValue > logic.value;
|
||||
case "greaterEqual":
|
||||
return answerValue >= logic.value;
|
||||
return logic.value !== undefined && answerValue >= logic.value;
|
||||
case "includesAll":
|
||||
return (
|
||||
Array.isArray(answerValue) &&
|
||||
|
||||
@@ -21,12 +21,14 @@ export default function CTAQuestionForm({
|
||||
lastQuestion,
|
||||
}: CTAQuestionFormProps): JSX.Element {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function LogicEditor({
|
||||
{localSurvey.questions.map(
|
||||
(question, idx) =>
|
||||
idx !== questionIdx && (
|
||||
<SelectItem key={question.id} value={question.id}>
|
||||
<SelectItem key={question.id} value={question.id} title={question.headline}>
|
||||
{idx + 1} - {truncate(question.headline, 14)}
|
||||
</SelectItem>
|
||||
)
|
||||
@@ -317,9 +317,10 @@ export default function LogicEditor({
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Button
|
||||
id="logicJumps"
|
||||
className="bg-slate-100 px-6 py-2 hover:bg-slate-50"
|
||||
className="bg-slate-100 hover:bg-slate-50"
|
||||
type="button"
|
||||
name="logicJumps"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
StartIcon={SplitIcon}
|
||||
onClick={() => addLogic()}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -19,6 +20,10 @@ export default function MultipleChoiceMultiForm({
|
||||
updateQuestion,
|
||||
lastQuestion,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: any) => {
|
||||
const newChoices = !question.choices
|
||||
? []
|
||||
@@ -32,6 +37,7 @@ export default function MultipleChoiceMultiForm({
|
||||
};
|
||||
|
||||
const addChoice = () => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
const otherChoice = newChoices.find((choice) => choice.id === "other");
|
||||
if (otherChoice) {
|
||||
@@ -70,12 +76,26 @@ export default function MultipleChoiceMultiForm({
|
||||
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastChoiceRef.current) {
|
||||
lastChoiceRef.current?.focus();
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
// This effect will run once on initial render, setting focus to the question input.
|
||||
useEffect(() => {
|
||||
if (isNew && questionRef.current) {
|
||||
questionRef.current.focus();
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
ref={questionRef}
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
@@ -103,6 +123,7 @@ export default function MultipleChoiceMultiForm({
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<div key={choiceIdx} className="inline-flex w-full items-center">
|
||||
<Input
|
||||
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
@@ -119,7 +140,7 @@ export default function MultipleChoiceMultiForm({
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
<Button variant="secondary" size="sm" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -19,6 +20,10 @@ export default function MultipleChoiceSingleForm({
|
||||
updateQuestion,
|
||||
lastQuestion,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: any) => {
|
||||
const newChoices = !question.choices
|
||||
? []
|
||||
@@ -32,6 +37,7 @@ export default function MultipleChoiceSingleForm({
|
||||
};
|
||||
|
||||
const addChoice = (choiceIdx?: number) => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
const otherChoice = newChoices.find((choice) => choice.id === "other");
|
||||
if (otherChoice) {
|
||||
@@ -75,12 +81,26 @@ export default function MultipleChoiceSingleForm({
|
||||
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastChoiceRef.current) {
|
||||
lastChoiceRef.current?.focus();
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
// This effect will run once on initial render, setting focus to the question input.
|
||||
useEffect(() => {
|
||||
if (isNew && questionRef.current) {
|
||||
questionRef.current.focus();
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
ref={questionRef}
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
@@ -114,6 +134,7 @@ export default function MultipleChoiceSingleForm({
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<div key={choiceIdx} className="inline-flex w-full items-center">
|
||||
<Input
|
||||
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
@@ -135,11 +156,9 @@ export default function MultipleChoiceSingleForm({
|
||||
))}
|
||||
<div className="flex items-center space-x-2">
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
<>
|
||||
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
|
||||
Add "Other" with specify
|
||||
</Button>
|
||||
</>
|
||||
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
|
||||
Add "Other" with specify
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function NPSQuestionForm({
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function OpenQuestionForm({
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function RatingQuestionForm({
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -73,6 +73,12 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
const handleTriggerDelay = (e: any) => {
|
||||
let value = parseInt(e.target.value);
|
||||
const updatedSurvey: Survey = { ...localSurvey, delay: value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.type === "link") {
|
||||
setOpen(false);
|
||||
@@ -199,6 +205,30 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
|
||||
Add condition
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{localSurvey.type !== "link" && (
|
||||
<div className="ml-2 flex items-center space-x-1 px-4 pb-4">
|
||||
<label
|
||||
htmlFor="triggerDelay"
|
||||
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div className="">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
Wait
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
id="triggerDelay"
|
||||
value={localSurvey.delay.toString()}
|
||||
onChange={(e) => handleTriggerDelay(e)}
|
||||
className="ml-2 mr-2 inline w-16 text-center text-sm"
|
||||
/>
|
||||
seconds before showing the survey.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ml-2 flex items-center space-x-1 p-4">
|
||||
<Switch id="autoClose" checked={autoClose} onCheckedChange={handleCheckMark} />
|
||||
<Label htmlFor="autoClose" className="cursor-pointer">
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function MultipleChoiceSummary({
|
||||
|
||||
const results: ChoiceResult[] = useMemo(() => {
|
||||
if (!("choices" in questionSummary.question)) return [];
|
||||
console.log(questionSummary.responses);
|
||||
|
||||
// build a dictionary of choices
|
||||
const resultsDict: { [key: string]: ChoiceResult } = {};
|
||||
for (const choice of questionSummary.question.choices) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkSurveyModal from "./LinkSurveyModal";
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
|
||||
export default function SummaryMetadata({ surveyId, environmentId }) {
|
||||
const { responsesData, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId);
|
||||
@@ -128,7 +129,9 @@ export default function SummaryMetadata({ surveyId, environmentId }) {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between lg:col-span-1">
|
||||
<div className=""></div>
|
||||
<div className="text-right text-xs text-slate-400">
|
||||
Last updated: {timeSinceConditionally(survey.updatedAt)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-1.5">
|
||||
{survey.type === "link" && (
|
||||
<Button
|
||||
|
||||
@@ -14,6 +14,8 @@ import { SparklesIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { customSurvey, templates } from "./templates";
|
||||
import { SplitIcon } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
|
||||
|
||||
type TemplateList = {
|
||||
environmentId: string;
|
||||
@@ -35,12 +37,6 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
|
||||
|
||||
const [categories, setCategories] = useState<Array<string>>([]);
|
||||
|
||||
/* useEffect(() => {
|
||||
if (product && templates?.length) {
|
||||
setActiveTemplate(customSurvey);
|
||||
}
|
||||
}, [product]); */
|
||||
|
||||
useEffect(() => {
|
||||
const defaultCategories = [
|
||||
/* ALL_CATEGORY_NAME, */
|
||||
@@ -74,7 +70,7 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
|
||||
|
||||
return (
|
||||
<main className="relative z-0 flex-1 overflow-y-auto px-6 pb-6 pt-3 focus:outline-none">
|
||||
<div className="mb-6 flex flex-wrap space-x-2">
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
@@ -103,7 +99,7 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
|
||||
activeTemplate?.name === customSurvey.name
|
||||
? "ring-brand border-transparent ring-2"
|
||||
: "hover:border-brand-dark border-dashed border-slate-300",
|
||||
"duration-120 group relative rounded-lg border-2 bg-transparent p-8 transition-colors duration-150"
|
||||
"duration-120 group relative rounded-lg border-2 bg-transparent p-6 transition-colors duration-150"
|
||||
)}>
|
||||
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
|
||||
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 ">{customSurvey.name}</h3>
|
||||
@@ -138,13 +134,41 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
|
||||
}}
|
||||
key={template.name}
|
||||
className={cn(
|
||||
activeTemplate?.name === template.name && "ring-brand ring-2",
|
||||
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105"
|
||||
activeTemplate?.name === template.name && "ring-2 ring-slate-400",
|
||||
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105"
|
||||
)}>
|
||||
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500">
|
||||
{template.category}
|
||||
<div className="flex">
|
||||
<div
|
||||
className={`rounded border px-1.5 py-0.5 text-xs ${
|
||||
template.category === "Product Experience"
|
||||
? "border-blue-300 bg-blue-50 text-blue-500"
|
||||
: template.category === "Exploration"
|
||||
? "border-pink-300 bg-pink-50 text-pink-500"
|
||||
: template.category === "Growth"
|
||||
? "border-orange-300 bg-orange-50 text-orange-500"
|
||||
: template.category === "Increase Revenue"
|
||||
? "border-emerald-300 bg-emerald-50 text-emerald-500"
|
||||
: template.category === "Customer Success"
|
||||
? "border-violet-300 bg-violet-50 text-violet-500"
|
||||
: "border-slate-300 bg-slate-50 text-slate-500" // default color
|
||||
}`}>
|
||||
{template.category}
|
||||
</div>
|
||||
{template.preset.questions.some(
|
||||
(question) => question.logic && question.logic.length > 0
|
||||
) && (
|
||||
<TooltipProvider delayDuration={80}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div>
|
||||
<SplitIcon className="ml-1.5 h-5 w-5 rounded border border-slate-300 bg-slate-50 p-0.5 text-slate-400" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>This survey uses branching logic.</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<template.icon className="h-8 w-8" />
|
||||
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{template.name}</h3>
|
||||
<p className="text-left text-xs text-slate-600">{template.description}</p>
|
||||
{activeTemplate?.name === template.name && (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,13 +72,13 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
|
||||
case "notEquals":
|
||||
return answerValue !== logic.value;
|
||||
case "lessThan":
|
||||
return answerValue < logic.value;
|
||||
return logic.value !== undefined && answerValue < logic.value;
|
||||
case "lessEqual":
|
||||
return answerValue <= logic.value;
|
||||
return logic.value !== undefined && answerValue <= logic.value;
|
||||
case "greaterThan":
|
||||
return answerValue > logic.value;
|
||||
return logic.value !== undefined && answerValue > logic.value;
|
||||
case "greaterEqual":
|
||||
return answerValue >= logic.value;
|
||||
return logic.value !== undefined && answerValue >= logic.value;
|
||||
case "includesAll":
|
||||
return (
|
||||
Array.isArray(answerValue) &&
|
||||
|
||||
@@ -11,6 +11,7 @@ import { GithubButton } from "./GithubButton";
|
||||
|
||||
export const SigninForm = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
setLoggingIn(true);
|
||||
@@ -48,6 +49,7 @@ export const SigninForm = () => {
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
ref={emailRef}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
@@ -90,6 +92,8 @@ export const SigninForm = () => {
|
||||
if (!showLogin) {
|
||||
setShowLogin(true);
|
||||
setButtonEnabled(false);
|
||||
// Add a slight delay before focusing the input field to ensure it's visible
|
||||
setTimeout(() => emailRef.current?.focus(), 100);
|
||||
} else if (formRef.current) {
|
||||
formRef.current.requestSubmit();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const SignupForm = () => {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string>("");
|
||||
const [signingUp, setSigningUp] = useState(false);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
setSigningUp(true);
|
||||
@@ -78,6 +79,7 @@ export const SignupForm = () => {
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
ref={nameRef}
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
@@ -136,6 +138,8 @@ export const SignupForm = () => {
|
||||
if (!showLogin) {
|
||||
setShowLogin(true);
|
||||
setButtonEnabled(false);
|
||||
// Add a slight delay before focusing the input field to ensure it's visible
|
||||
setTimeout(() => nameRef.current?.focus(), 100);
|
||||
} else if (formRef.current) {
|
||||
formRef.current.requestSubmit();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
|
||||
import { useEffect, useState } from "react";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
question: MultipleChoiceMultiQuestion;
|
||||
@@ -46,7 +46,7 @@ export default function MultipleChoiceMultiQuestion({
|
||||
};
|
||||
|
||||
onSubmit(data);
|
||||
// console.log(data);
|
||||
|
||||
setSelectedChoices([]); // reset value
|
||||
setShowOther(false);
|
||||
setOtherSpecified("");
|
||||
@@ -103,10 +103,9 @@ export default function MultipleChoiceMultiQuestion({
|
||||
</span>
|
||||
{choice.id === "other" && showOther && (
|
||||
<Input
|
||||
type="text"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
className="mt-2 bg-white"
|
||||
className="mt-2 bg-white focus:border-slate-300"
|
||||
placeholder="Please specify"
|
||||
onChange={(e) => setOtherSpecified(e.currentTarget.value)}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
|
||||
import { useState } from "react";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
question: MultipleChoiceSingleQuestion;
|
||||
@@ -21,19 +20,15 @@ export default function MultipleChoiceSingleQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const otherSpecify = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const value = otherSpecify.current?.value || e.currentTarget[question.id].value;
|
||||
const value = e.currentTarget[question.id].value;
|
||||
const data = {
|
||||
[question.id]: value,
|
||||
};
|
||||
// console.log(data);
|
||||
|
||||
onSubmit(data);
|
||||
setSelectedChoice(null); // reset form
|
||||
}}>
|
||||
@@ -70,12 +65,12 @@ export default function MultipleChoiceSingleQuestion({
|
||||
</span>
|
||||
{choice.id === "other" && selectedChoice === "other" && (
|
||||
<Input
|
||||
ref={otherSpecify}
|
||||
id="other-specify"
|
||||
name="other-specify"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="mt-3 bg-white"
|
||||
className="mt-3 bg-white focus:border-slate-300"
|
||||
required={question.required}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
export default function Progress({ progress, brandColor }: { progress: number; brandColor: string }) {
|
||||
import React from "react";
|
||||
|
||||
const ProgressComponent = ({ progress, brandColor }) => {
|
||||
return (
|
||||
<div className="h-1 w-full rounded-full bg-slate-200">
|
||||
<div
|
||||
className="h-1 rounded-full bg-slate-700"
|
||||
className="transition-width h-1 rounded-full duration-500"
|
||||
style={{ backgroundColor: brandColor, width: `${Math.floor(progress * 100)}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProgressComponent.displayName = "Progress";
|
||||
|
||||
const Progress = React.memo(ProgressComponent, (prevProps, nextProps) => {
|
||||
// Only re-render if progress or brandColor changes
|
||||
return prevProps.progress === nextProps.progress && prevProps.brandColor === nextProps.brandColor;
|
||||
});
|
||||
|
||||
export default Progress;
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function RatingQuestion({
|
||||
type="radio"
|
||||
name="rating"
|
||||
value={number}
|
||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||
className="absolute left-0 h-full w-full cursor-pointer opacity-0"
|
||||
onChange={() => handleSelect(number)}
|
||||
required={question.required}
|
||||
/>
|
||||
|
||||
@@ -108,6 +108,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
|
||||
},
|
||||
thankYouCard: true,
|
||||
autoClose: true,
|
||||
delay: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -182,6 +183,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
|
||||
triggers: survey.triggers,
|
||||
thankYouCard: JSON.parse(JSON.stringify(survey.thankYouCard)),
|
||||
autoClose: survey.autoClose,
|
||||
delay: survey.delay,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Survey" ADD COLUMN "delay" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -187,6 +187,7 @@ model Survey {
|
||||
attributeFilters SurveyAttributeFilter[]
|
||||
displays Display[]
|
||||
autoClose Int?
|
||||
delay Int @default(0)
|
||||
}
|
||||
|
||||
model Event {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MultipleChoiceMultiQuestion } from "../../../types/questions";
|
||||
import { h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useState, useRef, useEffect } from "preact/hooks";
|
||||
import { cn } from "../lib/utils";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
@@ -22,11 +22,18 @@ export default function MultipleChoiceMultiQuestion({
|
||||
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
|
||||
const [showOther, setShowOther] = useState(false);
|
||||
const [otherSpecified, setOtherSpecified] = useState("");
|
||||
const otherInputRef = useRef(null);
|
||||
|
||||
const isAtLeastOneChecked = () => {
|
||||
return selectedChoices.length > 0 || otherSpecified.length > 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showOther && otherInputRef.current) {
|
||||
otherInputRef.current.focus();
|
||||
}
|
||||
}, [showOther]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -97,11 +104,13 @@ export default function MultipleChoiceMultiQuestion({
|
||||
</span>
|
||||
{choice.id === "other" && showOther && (
|
||||
<input
|
||||
type="text"
|
||||
ref={otherInputRef}
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
className={cn(
|
||||
"fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
)}
|
||||
onChange={(e) => setOtherSpecified(e.currentTarget.value)}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
required={question.required}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h } from "preact";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { useRef, useState, useEffect } from "preact/hooks";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { MultipleChoiceSingleQuestion } from "../../../types/questions";
|
||||
import Headline from "./Headline";
|
||||
@@ -22,6 +22,12 @@ export default function MultipleChoiceSingleQuestion({
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const otherSpecify = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChoice === "other") {
|
||||
otherSpecify.current?.focus();
|
||||
}
|
||||
}, [selectedChoice]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -73,11 +79,12 @@ export default function MultipleChoiceSingleQuestion({
|
||||
{choice.id === "other" && selectedChoice === "other" && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id="other-specify"
|
||||
name="other-specify"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
required={question.required}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { h } from "preact";
|
||||
|
||||
export default function Progress({ progress, brandColor }: { progress: number; brandColor: string }) {
|
||||
return (
|
||||
<div className="fb-h-1 fb-w-full fb-rounded-full bg-slate-200">
|
||||
<div className="fb-h-1 fb-w-full fb-rounded-full fb-bg-slate-200">
|
||||
<div
|
||||
className="fb-h-1 fb-rounded-full"
|
||||
className="fb-h-1 fb-rounded-full fb-transition-width fb-duration-500"
|
||||
style={{ backgroundColor: brandColor, width: `${Math.floor(progress * 100)}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function RatingQuestion({
|
||||
type="radio"
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-absolute fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
className="fb-absolute fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0 fb-left-0"
|
||||
onChange={() => handleSelect(number)}
|
||||
required={question.required}
|
||||
/>
|
||||
|
||||
@@ -101,13 +101,13 @@ export default function SurveyView({ config, survey, close, errorHandler }: Surv
|
||||
case "notEquals":
|
||||
return answerValue !== logic.value;
|
||||
case "lessThan":
|
||||
return answerValue < logic.value;
|
||||
return logic.value !== undefined && answerValue < logic.value;
|
||||
case "lessEqual":
|
||||
return answerValue <= logic.value;
|
||||
return logic.value !== undefined && answerValue <= logic.value;
|
||||
case "greaterThan":
|
||||
return answerValue > logic.value;
|
||||
return logic.value !== undefined && answerValue > logic.value;
|
||||
case "greaterEqual":
|
||||
return answerValue >= logic.value;
|
||||
return logic.value !== undefined && answerValue >= logic.value;
|
||||
case "includesAll":
|
||||
return (
|
||||
Array.isArray(answerValue) &&
|
||||
|
||||
@@ -19,10 +19,12 @@ export const renderWidget = (survey: Survey) => {
|
||||
}
|
||||
surveyRunning = true;
|
||||
|
||||
render(
|
||||
h(App, { config: config.get(), survey, closeSurvey, errorHandler: errorHandler.handle }),
|
||||
document.getElementById(containerId)
|
||||
);
|
||||
setTimeout(() => {
|
||||
render(
|
||||
h(App, { config: config.get(), survey, closeSurvey, errorHandler: errorHandler.handle }),
|
||||
document.getElementById(containerId)
|
||||
);
|
||||
}, survey.delay * 1000);
|
||||
};
|
||||
|
||||
export const closeSurvey = async (): Promise<void> => {
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface Survey {
|
||||
triggers: Trigger[];
|
||||
thankYouCard: ThankYouCard;
|
||||
autoClose?: number | null;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export interface Trigger {
|
||||
|
||||
@@ -72,21 +72,21 @@ export type LogicCondition =
|
||||
|
||||
export interface LogicBase {
|
||||
condition: LogicCondition | undefined;
|
||||
value: number | string | string[] | undefined;
|
||||
value?: number | string | string[] | undefined;
|
||||
destination: string | "end" | undefined;
|
||||
}
|
||||
|
||||
export interface OpenTextLogic extends LogicBase {
|
||||
condition: "submitted" | "skipped" | undefined;
|
||||
value: undefined;
|
||||
value?: undefined;
|
||||
}
|
||||
export interface MultipleChoiceSingleLogic extends LogicBase {
|
||||
condition: "submitted" | "skipped" | "equals" | "notEquals" | undefined;
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
export interface MultipleChoiceMultiLogic extends LogicBase {
|
||||
condition: "submitted" | "skipped" | "includesAll" | "includesOne" | undefined;
|
||||
value: string[];
|
||||
value?: string[];
|
||||
}
|
||||
export interface NPSLogic extends LogicBase {
|
||||
condition:
|
||||
@@ -99,11 +99,11 @@ export interface NPSLogic extends LogicBase {
|
||||
| "equals"
|
||||
| "notEquals"
|
||||
| undefined;
|
||||
value: number;
|
||||
value?: number;
|
||||
}
|
||||
export interface CTALogic extends LogicBase {
|
||||
condition: "submitted" | "skipped" | undefined;
|
||||
value: undefined;
|
||||
value?: undefined;
|
||||
}
|
||||
export interface RatingLogic extends LogicBase {
|
||||
condition:
|
||||
@@ -116,7 +116,7 @@ export interface RatingLogic extends LogicBase {
|
||||
| "equals"
|
||||
| "notEquals"
|
||||
| undefined;
|
||||
value: number;
|
||||
value?: number | string;
|
||||
}
|
||||
export type Logic =
|
||||
| OpenTextLogic
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface Survey {
|
||||
displayOption: "displayOnce" | "displayMultiple" | "respondMultiple";
|
||||
attributeFilters: AttributeFilter[];
|
||||
autoClose: number | null;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export interface AttributeFilter {
|
||||
|
||||
@@ -10,8 +10,8 @@ export type Objective =
|
||||
|
||||
export interface Template {
|
||||
name: string;
|
||||
icon: any;
|
||||
description: string;
|
||||
icon?: any;
|
||||
category?: "Product Experience" | "Exploration" | "Growth" | "Increase Revenue" | "Customer Success";
|
||||
objectives?: [Objective, Objective?, Objective?];
|
||||
preset: {
|
||||
|
||||
Reference in New Issue
Block a user