Merge branch 'main' of https://github.com/Ashutosh-Bhadauriya/formbricks into enhance/question-card-ui

This commit is contained in:
AshutoshBhadauriya
2023-06-09 15:07:24 +05:30
36 changed files with 1545 additions and 482 deletions

View File

@@ -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

View File

@@ -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) => (

View File

@@ -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 = {

View File

@@ -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) &&

View File

@@ -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}

View File

@@ -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()}>

View File

@@ -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 && (

View File

@@ -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 &quot;Other&quot; with specify
</Button>
</>
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
Add &quot;Other&quot; with specify
</Button>
)}
</div>
</div>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 && (

View File

@@ -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) &&

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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`}

View File

@@ -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
/>
)}

View File

@@ -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;

View File

@@ -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}
/>

View File

@@ -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,
};
});

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "delay" INTEGER NOT NULL DEFAULT 0;

View File

@@ -187,6 +187,7 @@ model Survey {
attributeFilters SurveyAttributeFilter[]
displays Display[]
autoClose Int?
delay Int @default(0)
}
model Event {

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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}
/>

View File

@@ -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) &&

View File

@@ -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> => {

View File

@@ -80,6 +80,7 @@ export interface Survey {
triggers: Trigger[];
thankYouCard: ThankYouCard;
autoClose?: number | null;
delay: number;
}
export interface Trigger {

View File

@@ -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

View File

@@ -23,6 +23,7 @@ export interface Survey {
displayOption: "displayOnce" | "displayMultiple" | "respondMultiple";
attributeFilters: AttributeFilter[];
autoClose: number | null;
delay: number;
}
export interface AttributeFilter {

View File

@@ -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: {