mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 10:08:34 -06:00
feat: adds vercel style guide in surveys package WIP (#4401)
This commit is contained in:
@@ -190,7 +190,8 @@ const Page = async (props) => {
|
||||
<Link
|
||||
href="https://app.formbricks.com/s/clvupq3y205i5yrm3sm9v1xt5"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
rel="noopener noreferrer nofollow"
|
||||
referrerPolicy="no-referrer">
|
||||
{t("environments.settings.enterprise.request_30_day_trial_license")}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { SignupForm } from "./components/signup-form";
|
||||
|
||||
export const SignupPage = async ({ searchParams }) => {
|
||||
export const SignupPage = async ({ searchParams: searchParamsProps }) => {
|
||||
const searchParams = await searchParamsProps;
|
||||
const inviteToken = searchParams["inviteToken"] ?? null;
|
||||
const [isMultOrgEnabled, isSSOEnabled] = await Promise.all([getIsMultiOrgEnabled(), getIsSSOEnabled()]);
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Formbricks-surveys is a helper library to embed surveys into your application.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/formbricks/formbricks"
|
||||
@@ -42,6 +42,7 @@
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@preact/preset-vite": "2.9.0",
|
||||
"@types/react": "18.3.11",
|
||||
"autoprefixer": "10.4.20",
|
||||
"concurrently": "8.2.2",
|
||||
"isomorphic-dompurify": "2.14.0",
|
||||
@@ -56,6 +57,7 @@
|
||||
"vite-tsconfig-paths": "5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "0.8.2"
|
||||
"@formkit/auto-animate": "0.8.2",
|
||||
"react-calendar": "5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@ interface BackButtonProps {
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export const BackButton = ({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) => {
|
||||
export function BackButton({ onClick, backButtonLabel, tabIndex = 2 }: BackButtonProps) {
|
||||
return (
|
||||
<button
|
||||
dir="auto"
|
||||
tabIndex={tabIndex}
|
||||
type={"button"}
|
||||
type="button"
|
||||
className={cn(
|
||||
"fb-border-back-button-border fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-flex fb-items-center fb-border fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 fb-shadow-sm hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"
|
||||
)}
|
||||
onClick={onClick}>
|
||||
{backButtonLabel || "Back"}
|
||||
{backButtonLabel ?? "Back"}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSX } from "preact";
|
||||
import { type JSX } from "preact";
|
||||
import { useCallback } from "preact/hooks";
|
||||
|
||||
interface SubmitButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
|
||||
@@ -7,7 +7,7 @@ interface SubmitButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
|
||||
focus?: boolean;
|
||||
}
|
||||
|
||||
export const SubmitButton = ({
|
||||
export function SubmitButton({
|
||||
buttonLabel,
|
||||
isLastQuestion,
|
||||
tabIndex = 1,
|
||||
@@ -16,7 +16,7 @@ export const SubmitButton = ({
|
||||
disabled,
|
||||
type,
|
||||
...props
|
||||
}: SubmitButtonProps) => {
|
||||
}: SubmitButtonProps) {
|
||||
const buttonRef = useCallback(
|
||||
(currentButton: HTMLButtonElement | null) => {
|
||||
if (currentButton && focus) {
|
||||
@@ -42,4 +42,4 @@ export const SubmitButton = ({
|
||||
{buttonLabel || (isLastQuestion ? "Finish" : "Next")}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { GlobeIcon } from "@/components/general/GlobeIcon";
|
||||
import { useRef, useState } from "react";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
|
||||
import { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface LanguageSwitchProps {
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
setFirstRender?: (firstRender: boolean) => void;
|
||||
}
|
||||
export const LanguageSwitch = ({
|
||||
surveyLanguages,
|
||||
setSelectedLanguageCode,
|
||||
setFirstRender,
|
||||
}: LanguageSwitchProps) => {
|
||||
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
|
||||
const toggleDropdown = () => setShowLanguageDropdown((prev) => !prev);
|
||||
const languageDropdownRef = useRef(null);
|
||||
const defaultLanguageCode = surveyLanguages.find((surveyLanguage) => {
|
||||
return surveyLanguage.default === true;
|
||||
})?.language.code;
|
||||
|
||||
const changeLanguage = (languageCode: string) => {
|
||||
if (languageCode === defaultLanguageCode) {
|
||||
setSelectedLanguageCode("default");
|
||||
} else {
|
||||
setSelectedLanguageCode(languageCode);
|
||||
}
|
||||
if (setFirstRender) {
|
||||
//for lexical editor
|
||||
setFirstRender(true);
|
||||
}
|
||||
setShowLanguageDropdown(false);
|
||||
};
|
||||
|
||||
useClickOutside(languageDropdownRef, () => setShowLanguageDropdown(false));
|
||||
|
||||
return (
|
||||
<div class="fb-z-[1001] fb-flex fb-w-fit fb-items-center even:fb-pr-1">
|
||||
<button
|
||||
title="Language switch"
|
||||
type="button"
|
||||
class="fb-text-heading fb-relative fb-h-5 fb-w-5 fb-rounded-md hover:fb-bg-black/5 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"
|
||||
onClick={toggleDropdown}
|
||||
tabIndex={-1}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={showLanguageDropdown}>
|
||||
<GlobeIcon className="fb-text-heading fb-h-5 fb-w-5 fb-p-0.5" />
|
||||
</button>
|
||||
{showLanguageDropdown && (
|
||||
<div
|
||||
className="fb-bg-brand fb-text-on-brand fb-absolute fb-right-8 fb-top-10 fb-space-y-2 fb-rounded-md fb-p-2 fb-text-xs"
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map((surveyLanguage) => {
|
||||
if (!surveyLanguage.enabled) return;
|
||||
return (
|
||||
<button
|
||||
key={surveyLanguage.language.id}
|
||||
type="button"
|
||||
className="fb-block fb-w-full fb-p-1.5 fb-text-left hover:fb-opacity-80"
|
||||
onClick={() => changeLanguage(surveyLanguage.language.code)}>
|
||||
{getLanguageLabel(surveyLanguage.language.code, "en-US")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { processResponseData } from "@formbricks/lib/responses";
|
||||
import { type TResponseData } from "@formbricks/types/responses";
|
||||
import { type TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
type ResponseErrorComponentProps = {
|
||||
questions: TSurveyQuestion[];
|
||||
responseData: TResponseData;
|
||||
onRetry: () => void;
|
||||
};
|
||||
|
||||
export const ResponseErrorComponent = ({ questions, responseData, onRetry }: ResponseErrorComponentProps) => {
|
||||
return (
|
||||
<div className={"fb-flex fb-flex-col fb-bg-white fb-p-4"}>
|
||||
<span className={"fb-mb-1.5 fb-text-base fb-font-bold fb-leading-6 fb-text-slate-900"}>
|
||||
{"Your feedback is stuck :("}
|
||||
</span>
|
||||
<p className={"fb-max-w-md fb-text-sm fb-font-normal fb-leading-6 fb-text-slate-600"}>
|
||||
The servers cannot be reached at the moment.
|
||||
<br />
|
||||
Please retry now or try again later.
|
||||
</p>
|
||||
<div className={"fb-mt-4 fb-rounded-lg fb-border fb-border-slate-200 fb-bg-slate-100 fb-px-4 fb-py-5"}>
|
||||
<div className={"fb-flex fb-max-h-36 fb-flex-1 fb-flex-col fb-space-y-3 fb-overflow-y-scroll"}>
|
||||
{questions.map((question, index) => {
|
||||
const response = responseData[question.id];
|
||||
if (!response) return;
|
||||
return (
|
||||
<div className={"fb-flex fb-flex-col"}>
|
||||
<span className={"fb-text-sm fb-leading-6 fb-text-slate-900"}>{`Question ${index + 1}`}</span>
|
||||
<span className={"fb-mt-1 fb-text-sm fb-font-semibold fb-leading-6 fb-text-slate-900"}>
|
||||
{processResponseData(response)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"fb-mt-4 fb-flex fb-flex-1 fb-flex-row fb-items-center fb-justify-end fb-space-x-2"}>
|
||||
<SubmitButton tabIndex={2} buttonLabel="Retry" isLastQuestion={false} onClick={() => onRetry()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,15 +2,15 @@ interface AutoCloseProgressBarProps {
|
||||
autoCloseTimeout: number;
|
||||
}
|
||||
|
||||
export const AutoCloseProgressBar = ({ autoCloseTimeout }: AutoCloseProgressBarProps) => {
|
||||
export function AutoCloseProgressBar({ autoCloseTimeout }: AutoCloseProgressBarProps) {
|
||||
return (
|
||||
<div className="fb-bg-accent-bg fb-h-2 fb-w-full fb-overflow-hidden fb-rounded-full">
|
||||
<div
|
||||
key={autoCloseTimeout}
|
||||
className="fb-bg-brand fb-z-20 fb-h-2 fb-rounded-full"
|
||||
style={{
|
||||
animation: `shrink-width-to-zero ${autoCloseTimeout}s linear forwards`,
|
||||
}}></div>
|
||||
animation: `shrink-width-to-zero ${autoCloseTimeout.toString()}s linear forwards`,
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import snippet from "@calcom/embed-snippet";
|
||||
import { useEffect, useMemo } from "preact/hooks";
|
||||
import { TSurveyCalQuestion } from "@formbricks/types/surveys/types";
|
||||
import { type TSurveyCalQuestion } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CalEmbedProps {
|
||||
question: TSurveyCalQuestion;
|
||||
onSuccessfulBooking: () => void;
|
||||
}
|
||||
|
||||
export const CalEmbed = ({ question, onSuccessfulBooking }: CalEmbedProps) => {
|
||||
export function CalEmbed({ question, onSuccessfulBooking }: CalEmbedProps) {
|
||||
const cal = useMemo(() => {
|
||||
const calInline = snippet("https://cal.com/embed.js");
|
||||
|
||||
@@ -43,7 +43,7 @@ export const CalEmbed = ({ question, onSuccessfulBooking }: CalEmbedProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
// remove any existing cal-inline elements
|
||||
document.querySelectorAll("cal-inline").forEach((el) => el.remove());
|
||||
document.querySelectorAll("cal-inline").forEach((el) => { el.remove(); });
|
||||
cal("init", { calOrigin: question.calHost ? `https://${question.calHost}` : "https://cal.com" });
|
||||
cal("inline", {
|
||||
elementOrSelector: "#fb-cal-embed",
|
||||
@@ -56,4 +56,4 @@ export const CalEmbed = ({ question, onSuccessfulBooking }: CalEmbedProps) => {
|
||||
<div id="fb-cal-embed" className={cn("fb-border-border fb-rounded-lg fb-border")} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { LoadingSpinner } from "@/components/general/LoadingSpinner";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { LoadingSpinner } from "@/components/general/loading-spinner";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { replaceRecallInfo } from "@/lib/recall";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import { TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||
import { type TSurveyEndScreenCard, type TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface EndingCardProps {
|
||||
survey: TJsEnvironmentStateSurvey;
|
||||
@@ -23,7 +23,7 @@ interface EndingCardProps {
|
||||
variablesData: TResponseVariables;
|
||||
}
|
||||
|
||||
export const EndingCard = ({
|
||||
export function EndingCard({
|
||||
survey,
|
||||
endingCard,
|
||||
isRedirectDisabled,
|
||||
@@ -33,9 +33,9 @@ export const EndingCard = ({
|
||||
languageCode,
|
||||
responseData,
|
||||
variablesData,
|
||||
}: EndingCardProps) => {
|
||||
}: EndingCardProps) {
|
||||
const media =
|
||||
endingCard.type === "endScreen" && (endingCard.imageUrl || endingCard.videoUrl) ? (
|
||||
endingCard.type === "endScreen" && (endingCard.imageUrl ?? endingCard.videoUrl) ? (
|
||||
<QuestionMedia imgUrl={endingCard.imageUrl} videoUrl={endingCard.videoUrl} />
|
||||
) : null;
|
||||
const checkmark = (
|
||||
@@ -46,14 +46,14 @@ export const EndingCard = ({
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
class="fb-h-24 fb-w-24">
|
||||
className="fb-h-24 fb-w-24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="fb-bg-brand fb-mb-[10px] fb-inline-block fb-h-1 fb-w-16 fb-rounded-[100%]"></span>
|
||||
<span className="fb-bg-brand fb-mb-[10px] fb-inline-block fb-h-1 fb-w-16 fb-rounded-[100%]" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -85,7 +85,7 @@ export const EndingCard = ({
|
||||
document.removeEventListener("keydown", handleEnter);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this effect when isCurrent changes
|
||||
}, [isCurrent]);
|
||||
|
||||
return (
|
||||
@@ -93,10 +93,10 @@ export const EndingCard = ({
|
||||
<div className="fb-text-center">
|
||||
{isResponseSendingFinished ? (
|
||||
<>
|
||||
{endingCard.type === "endScreen" && (media || checkmark)}
|
||||
{endingCard.type === "endScreen" && (media ?? checkmark)}
|
||||
<div>
|
||||
<Headline
|
||||
alignTextCenter={true}
|
||||
alignTextCenter
|
||||
headline={
|
||||
endingCard.type === "endScreen"
|
||||
? replaceRecallInfo(
|
||||
@@ -120,7 +120,7 @@ export const EndingCard = ({
|
||||
}
|
||||
questionId="EndingCard"
|
||||
/>
|
||||
{endingCard.type === "endScreen" && endingCard.buttonLabel && (
|
||||
{endingCard.type === "endScreen" && endingCard.buttonLabel ? (
|
||||
<div className="fb-mt-6 fb-flex fb-w-full fb-flex-col fb-items-center fb-justify-center fb-space-y-4">
|
||||
<SubmitButton
|
||||
buttonLabel={replaceRecallInfo(
|
||||
@@ -133,7 +133,7 @@ export const EndingCard = ({
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@@ -147,4 +147,4 @@ export const EndingCard = ({
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { JSXInternal } from "preact/src/jsx";
|
||||
import { type JSXInternal } from "preact/src/jsx";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { isFulfilled, isRejected } from "@formbricks/lib/utils/promises";
|
||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { type TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
|
||||
interface FileInputProps {
|
||||
allowedFileExtensions?: TAllowedFileExtension[];
|
||||
@@ -20,7 +20,7 @@ interface FileInputProps {
|
||||
|
||||
const FILE_LIMIT = 25;
|
||||
|
||||
export const FileInput = ({
|
||||
export function FileInput({
|
||||
allowedFileExtensions,
|
||||
surveyId,
|
||||
onFileUpload,
|
||||
@@ -29,7 +29,7 @@ export const FileInput = ({
|
||||
maxSizeInMB,
|
||||
allowMultipleFiles,
|
||||
htmlFor = "",
|
||||
}: FileInputProps) => {
|
||||
}: FileInputProps) {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [parent] = useAutoAnimate();
|
||||
@@ -39,7 +39,7 @@ export const FileInput = ({
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
const bufferKB = fileBuffer.byteLength / 1024;
|
||||
if (bufferKB > maxSizeInMB * 1024) {
|
||||
alert(`File should be less than ${maxSizeInMB} MB`);
|
||||
alert(`File should be less than ${maxSizeInMB.toString()} MB`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export const FileInput = ({
|
||||
}
|
||||
|
||||
if (allowMultipleFiles && selectedFiles.length + fileArray.length > FILE_LIMIT) {
|
||||
alert(`You can only upload a maximum of ${FILE_LIMIT} files.`);
|
||||
alert(`You can only upload a maximum of ${FILE_LIMIT.toString()} files.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,10 +63,9 @@ export const FileInput = ({
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const fileExtension = file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension;
|
||||
if (allowedFileExtensions) {
|
||||
return allowedFileExtensions?.includes(fileExtension);
|
||||
} else {
|
||||
return true;
|
||||
return allowedFileExtensions.includes(fileExtension);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const filteredFiles: File[] = [];
|
||||
@@ -84,7 +83,9 @@ export const FileInput = ({
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
|
||||
@@ -122,16 +123,16 @@ export const FileInput = ({
|
||||
const handleDragOver = (e: JSXInternal.TargetedDragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error -- TS does not recognize dataTransfer
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const handleDrop = (e: JSXInternal.TargetedDragEvent<HTMLLabelElement>) => {
|
||||
const handleDrop = async (e: JSXInternal.TargetedDragEvent<HTMLLabelElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// @ts-expect-error
|
||||
handleFileSelection(e.dataTransfer.files);
|
||||
// @ts-expect-error -- TS does not recognize dataTransfer
|
||||
await handleFileSelection(e.dataTransfer.files);
|
||||
};
|
||||
|
||||
const handleDeleteFile = (index: number, event: JSXInternal.TargetedMouseEvent<SVGSVGElement>) => {
|
||||
@@ -157,8 +158,7 @@ export const FileInput = ({
|
||||
const uniqueHtmlFor = useMemo(() => `selectedFile-${htmlFor}`, [htmlFor]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fb-items-left fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-relative fb-mt-3 fb-flex fb-w-full fb-flex-col fb-justify-center fb-rounded-lg fb-border-2 fb-border-dashed dark:fb-border-slate-600 dark:fb-bg-slate-700 dark:hover:fb-border-slate-500 dark:hover:fb-bg-slate-800`}>
|
||||
<div className="fb-items-left fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-relative fb-mt-3 fb-flex fb-w-full fb-flex-col fb-justify-center fb-rounded-lg fb-border-2 fb-border-dashed dark:fb-border-slate-600 dark:fb-bg-slate-700 dark:hover:fb-border-slate-500 dark:hover:fb-bg-slate-800">
|
||||
<div ref={parent}>
|
||||
{fileUrls?.map((fileUrl, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
@@ -175,7 +175,9 @@ export const FileInput = ({
|
||||
strokeWidth={1}
|
||||
stroke="currentColor"
|
||||
className="fb-text-heading fb-h-5"
|
||||
onClick={(e) => handleDeleteFile(index, e)}>
|
||||
onClick={(e) => {
|
||||
handleDeleteFile(index, e);
|
||||
}}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -205,16 +207,16 @@ export const FileInput = ({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{isUploading && (
|
||||
{isUploading ? (
|
||||
<div className="fb-inset-0 fb-flex fb-animate-pulse fb-items-center fb-justify-center fb-rounded-lg fb-py-4">
|
||||
<label htmlFor={uniqueHtmlFor} className="fb-text-subheading fb-text-sm fb-font-medium">
|
||||
Uploading...
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<label htmlFor={uniqueHtmlFor} onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||
{showUploader && (
|
||||
{showUploader ? (
|
||||
<div
|
||||
className="focus:fb-outline-brand fb-flex fb-flex-col fb-items-center fb-justify-center fb-py-6 hover:fb-cursor-pointer"
|
||||
tabIndex={1}
|
||||
@@ -251,15 +253,15 @@ export const FileInput = ({
|
||||
onChange={async (e) => {
|
||||
const inputElement = e.target as HTMLInputElement;
|
||||
if (inputElement.files) {
|
||||
handleFileSelection(inputElement.files);
|
||||
await handleFileSelection(inputElement.files);
|
||||
}
|
||||
}}
|
||||
multiple={allowMultipleFiles}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
export const FormbricksBranding = () => {
|
||||
export function FormbricksBranding() {
|
||||
return (
|
||||
<a
|
||||
href="https://formbricks.com?utm_source=survey_branding"
|
||||
target="_blank"
|
||||
tabIndex={-1}
|
||||
className="fb-my-2 fb-flex fb-justify-center">
|
||||
className="fb-my-2 fb-flex fb-justify-center" rel="noopener">
|
||||
<p className="fb-text-signature fb-text-xs">
|
||||
Powered by{" "}
|
||||
<b>
|
||||
@@ -13,4 +13,4 @@ export const FormbricksBranding = () => {
|
||||
</p>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -2,21 +2,20 @@ interface GlobeIconProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GlobeIcon = ({ className }: GlobeIconProps) => {
|
||||
export function GlobeIcon({ className }: GlobeIconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
className={`lucide lucide-globe ${className ? className.toString() : ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-globe">
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
||||
<path d="M2 12h20" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface HeadlineProps {
|
||||
headline?: string;
|
||||
@@ -6,13 +6,7 @@ interface HeadlineProps {
|
||||
required?: boolean;
|
||||
alignTextCenter?: boolean;
|
||||
}
|
||||
|
||||
export const Headline = ({
|
||||
headline,
|
||||
questionId,
|
||||
required = true,
|
||||
alignTextCenter = false,
|
||||
}: HeadlineProps) => {
|
||||
export function Headline({ headline, questionId, required = true, alignTextCenter = false }: HeadlineProps) {
|
||||
return (
|
||||
<label
|
||||
htmlFor={questionId}
|
||||
@@ -31,4 +25,4 @@ export const Headline = ({
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface HtmlBodyProps {
|
||||
htmlString?: string;
|
||||
questionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const HtmlBody = ({ htmlString, questionId }: HtmlBodyProps) => {
|
||||
export function HtmlBody({ htmlString, questionId }: HtmlBodyProps) {
|
||||
const [safeHtml, setSafeHtml] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (htmlString) {
|
||||
import("isomorphic-dompurify").then((DOMPurify) => {
|
||||
setSafeHtml(DOMPurify.sanitize(htmlString, { ADD_ATTR: ["target"] }));
|
||||
});
|
||||
setSafeHtml(DOMPurify.sanitize(htmlString, { ADD_ATTR: ["target"] }));
|
||||
}
|
||||
}, [htmlString]);
|
||||
|
||||
@@ -29,4 +28,4 @@ export const HtmlBody = ({ htmlString, questionId }: HtmlBodyProps) => {
|
||||
dir="auto"
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HTMLAttributes } from "preact/compat";
|
||||
import { forwardRef } from "preact/compat";
|
||||
import { type HTMLAttributes, forwardRef } from "preact/compat";
|
||||
|
||||
export interface InputProps extends HTMLAttributes<HTMLInputElement> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
69
packages/surveys/src/components/general/language-switch.tsx
Normal file
69
packages/surveys/src/components/general/language-switch.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
|
||||
import { type TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { GlobeIcon } from "@/components/general/globe-icon";
|
||||
|
||||
interface LanguageSwitchProps {
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
setFirstRender?: (firstRender: boolean) => void;
|
||||
}
|
||||
export function LanguageSwitch({
|
||||
surveyLanguages,
|
||||
setSelectedLanguageCode,
|
||||
setFirstRender,
|
||||
}: LanguageSwitchProps) {
|
||||
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
|
||||
const toggleDropdown = () => { setShowLanguageDropdown((prev) => !prev); };
|
||||
const languageDropdownRef = useRef(null);
|
||||
const defaultLanguageCode = surveyLanguages.find((surveyLanguage) => {
|
||||
return surveyLanguage.default;
|
||||
})?.language.code;
|
||||
|
||||
const changeLanguage = (languageCode: string) => {
|
||||
if (languageCode === defaultLanguageCode) {
|
||||
setSelectedLanguageCode("default");
|
||||
} else {
|
||||
setSelectedLanguageCode(languageCode);
|
||||
}
|
||||
if (setFirstRender) {
|
||||
//for lexical editor
|
||||
setFirstRender(true);
|
||||
}
|
||||
setShowLanguageDropdown(false);
|
||||
};
|
||||
|
||||
useClickOutside(languageDropdownRef, () => { setShowLanguageDropdown(false); });
|
||||
|
||||
return (
|
||||
<div className="fb-z-[1001] fb-flex fb-w-fit fb-items-center even:fb-pr-1">
|
||||
<button
|
||||
title="Language switch"
|
||||
type="button"
|
||||
className="fb-text-heading fb-relative fb-h-5 fb-w-5 fb-rounded-md hover:fb-bg-black/5 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"
|
||||
onClick={toggleDropdown}
|
||||
tabIndex={-1}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={showLanguageDropdown}>
|
||||
<GlobeIcon className="fb-text-heading fb-h-5 fb-w-5 fb-p-0.5" />
|
||||
</button>
|
||||
{showLanguageDropdown ? <div
|
||||
className="fb-bg-brand fb-text-on-brand fb-absolute fb-right-8 fb-top-10 fb-space-y-2 fb-rounded-md fb-p-2 fb-text-xs"
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map((surveyLanguage) => {
|
||||
if (!surveyLanguage.enabled) return;
|
||||
return (
|
||||
<button
|
||||
key={surveyLanguage.language.id}
|
||||
type="button"
|
||||
className="fb-block fb-w-full fb-p-1.5 fb-text-left hover:fb-opacity-80"
|
||||
onClick={() => { changeLanguage(surveyLanguage.language.code); }}>
|
||||
{getLanguageLabel(surveyLanguage.language.code, "en-US")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const LoadingSpinner = ({ className }: { className?: string }) => {
|
||||
export function LoadingSpinner({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
data-testid="loading-spinner"
|
||||
@@ -16,12 +16,12 @@ export const LoadingSpinner = ({ className }: { className?: string }) => {
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"></circle>
|
||||
strokeWidth="4" />
|
||||
<path
|
||||
className="fb-opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Progress } from "@/components/general/progress";
|
||||
import { calculateElementIdx } from "@/lib/utils";
|
||||
import { useCallback, useMemo } from "preact/hooks";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { Progress } from "./Progress";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ProgressBarProps {
|
||||
survey: TJsEnvironmentStateSurvey;
|
||||
questionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const ProgressBar = ({ survey, questionId }: ProgressBarProps) => {
|
||||
export function ProgressBar({ survey, questionId }: ProgressBarProps) {
|
||||
const currentQuestionIdx = useMemo(
|
||||
() => survey.questions.findIndex((q) => q.id === questionId),
|
||||
[survey, questionId]
|
||||
@@ -18,10 +18,12 @@ export const ProgressBar = ({ survey, questionId }: ProgressBarProps) => {
|
||||
|
||||
const calculateProgress = useCallback(
|
||||
(index: number, questionsLength: number) => {
|
||||
if (questionsLength === 0) return 0;
|
||||
if (index === -1) index = 0;
|
||||
let idx = index;
|
||||
|
||||
const elementIdx = calculateElementIdx(survey, index);
|
||||
if (questionsLength === 0) return 0;
|
||||
if (index === -1) idx = 0;
|
||||
|
||||
const elementIdx = calculateElementIdx(survey, idx);
|
||||
return elementIdx / questionsLength;
|
||||
},
|
||||
[survey]
|
||||
@@ -36,10 +38,9 @@ export const ProgressBar = ({ survey, questionId }: ProgressBarProps) => {
|
||||
return 0;
|
||||
} else if (endingCardIds.includes(questionId)) {
|
||||
return 1;
|
||||
} else {
|
||||
return progressArray[currentQuestionIdx];
|
||||
}
|
||||
return progressArray[currentQuestionIdx];
|
||||
}, [questionId, endingCardIds, progressArray, currentQuestionIdx]);
|
||||
|
||||
return <Progress progress={progressValue} />;
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
export const Progress = ({ progress }: { progress: number }) => {
|
||||
export function Progress({ progress }: { progress: number }) {
|
||||
return (
|
||||
<div className="fb-bg-accent-bg fb-h-2 fb-w-full fb-overflow-hidden fb-rounded-full">
|
||||
<div
|
||||
className="fb-transition-width fb-bg-brand fb-z-20 fb-h-2 fb-rounded-full fb-duration-500"
|
||||
style={{ width: `${Math.floor(progress * 100)}%` }}></div>
|
||||
style={{ width: `${Math.floor(progress * 100).toString()}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
import { AddressQuestion } from "@/components/questions/AddressQuestion";
|
||||
import { CTAQuestion } from "@/components/questions/CTAQuestion";
|
||||
import { CalQuestion } from "@/components/questions/CalQuestion";
|
||||
import { ConsentQuestion } from "@/components/questions/ConsentQuestion";
|
||||
import { ContactInfoQuestion } from "@/components/questions/ContactInfoQuestion";
|
||||
import { DateQuestion } from "@/components/questions/DateQuestion";
|
||||
import { FileUploadQuestion } from "@/components/questions/FileUploadQuestion";
|
||||
import { MatrixQuestion } from "@/components/questions/MatrixQuestion";
|
||||
import { MultipleChoiceMultiQuestion } from "@/components/questions/MultipleChoiceMultiQuestion";
|
||||
import { MultipleChoiceSingleQuestion } from "@/components/questions/MultipleChoiceSingleQuestion";
|
||||
import { NPSQuestion } from "@/components/questions/NPSQuestion";
|
||||
import { OpenTextQuestion } from "@/components/questions/OpenTextQuestion";
|
||||
import { PictureSelectionQuestion } from "@/components/questions/PictureSelectionQuestion";
|
||||
import { RankingQuestion } from "@/components/questions/RankingQuestion";
|
||||
import { RatingQuestion } from "@/components/questions/RatingQuestion";
|
||||
import { AddressQuestion } from "@/components/questions/address-question";
|
||||
import { CalQuestion } from "@/components/questions/cal-question";
|
||||
import { ConsentQuestion } from "@/components/questions/consent-question";
|
||||
import { ContactInfoQuestion } from "@/components/questions/contact-info-question";
|
||||
import { CTAQuestion } from "@/components/questions/cta-question";
|
||||
import { DateQuestion } from "@/components/questions/date-question";
|
||||
import { FileUploadQuestion } from "@/components/questions/file-upload-question";
|
||||
import { MatrixQuestion } from "@/components/questions/matrix-question";
|
||||
import { MultipleChoiceMultiQuestion } from "@/components/questions/multiple-choice-multi-question";
|
||||
import { MultipleChoiceSingleQuestion } from "@/components/questions/multiple-choice-single-question";
|
||||
import { NPSQuestion } from "@/components/questions/nps-question";
|
||||
import { OpenTextQuestion } from "@/components/questions/open-text-question";
|
||||
import { PictureSelectionQuestion } from "@/components/questions/picture-selection-question";
|
||||
import { RankingQuestion } from "@/components/questions/ranking-question";
|
||||
import { RatingQuestion } from "@/components/questions/rating-question";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseDataValue, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { type TResponseData, type TResponseDataValue, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import {
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
TSurveyQuestionId,
|
||||
type TSurveyQuestion,
|
||||
type TSurveyQuestionChoice,
|
||||
type TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -43,7 +43,7 @@ interface QuestionConditionalProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const QuestionConditional = ({
|
||||
export function QuestionConditional({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -60,7 +60,7 @@ export const QuestionConditional = ({
|
||||
onFileUpload,
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
}: QuestionConditionalProps) => {
|
||||
}: QuestionConditionalProps) {
|
||||
const getResponseValueForRankingQuestion = (
|
||||
value: string[],
|
||||
choices: TSurveyQuestionChoice[]
|
||||
@@ -316,4 +316,4 @@ export const QuestionConditional = ({
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const getVideoUrlWithParams = (videoUrl: string): string => {
|
||||
"?title=false&transcript=false&speed=false&quality_selector=false&progress_bar=false&pip=false&fullscreen=false&cc=false&chromecast=false"
|
||||
);
|
||||
else if (isLoomUrl) return videoUrl.concat("?hide_share=true&hideEmbedTopBar=true&hide_title=true");
|
||||
else return videoUrl;
|
||||
return videoUrl;
|
||||
};
|
||||
|
||||
interface QuestionMediaProps {
|
||||
@@ -26,16 +26,16 @@ interface QuestionMediaProps {
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionMediaProps) => {
|
||||
export function QuestionMedia({ imgUrl, videoUrl, altText = "Image" }: QuestionMediaProps) {
|
||||
const videoUrlWithParams = videoUrl ? getVideoUrlWithParams(videoUrl) : undefined;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="fb-group/image fb-relative fb-mb-4 fb-block fb-min-h-40 fb-rounded-md">
|
||||
{isLoading && (
|
||||
{isLoading ? (
|
||||
<div className="fb-absolute fb-inset-auto fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
|
||||
)}
|
||||
{imgUrl && (
|
||||
) : null}
|
||||
{imgUrl ? (
|
||||
<img
|
||||
key={imgUrl}
|
||||
src={imgUrl}
|
||||
@@ -45,23 +45,26 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
setIsLoading(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{videoUrlWithParams && (
|
||||
) : null}
|
||||
{videoUrlWithParams ? (
|
||||
<div className="fb-relative">
|
||||
<div className="fb-rounded-custom fb-bg-black">
|
||||
<iframe
|
||||
src={videoUrlWithParams}
|
||||
title="Question Video"
|
||||
frameborder="0"
|
||||
frameBorder="0"
|
||||
className="fb-rounded-custom fb-aspect-video fb-w-full"
|
||||
onLoad={() => setIsLoading(false)}
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"></iframe>
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
<a
|
||||
href={!!imgUrl ? imgUrl : parseVideoUrl(videoUrl ?? "")}
|
||||
href={imgUrl ? imgUrl : parseVideoUrl(videoUrl ?? "")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="fb-absolute fb-bottom-2 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-opacity-0 fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100">
|
||||
@@ -75,7 +78,7 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
class="lucide lucide-expand">
|
||||
className="lucide lucide-expand">
|
||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
||||
@@ -84,4 +87,4 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -7,13 +7,13 @@ interface RedirectCountDownProps {
|
||||
isRedirectDisabled: boolean;
|
||||
}
|
||||
|
||||
export const RedirectCountDown = ({ redirectUrl, isRedirectDisabled }: RedirectCountDownProps) => {
|
||||
export function RedirectCountDown({ redirectUrl, isRedirectDisabled }: RedirectCountDownProps) {
|
||||
const [timeRemaining, setTimeRemaining] = useState(REDIRECT_TIMEOUT);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout | undefined;
|
||||
if (redirectUrl) {
|
||||
const interval = setInterval(() => {
|
||||
interval = setInterval(() => {
|
||||
setTimeRemaining((prevTime) => {
|
||||
if (prevTime <= 0) {
|
||||
clearInterval(interval);
|
||||
@@ -28,7 +28,7 @@ export const RedirectCountDown = ({ redirectUrl, isRedirectDisabled }: RedirectC
|
||||
}
|
||||
|
||||
// Clean up the interval when the component is unmounted
|
||||
return () => clearInterval(interval);
|
||||
return () => { clearInterval(interval); };
|
||||
}, [redirectUrl, isRedirectDisabled]);
|
||||
|
||||
if (!redirectUrl) return null;
|
||||
@@ -41,4 +41,4 @@ export const RedirectCountDown = ({ redirectUrl, isRedirectDisabled }: RedirectC
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { processResponseData } from "@formbricks/lib/responses";
|
||||
import { type TResponseData } from "@formbricks/types/responses";
|
||||
import { type TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ResponseErrorComponentProps {
|
||||
questions: TSurveyQuestion[];
|
||||
responseData: TResponseData;
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
export function ResponseErrorComponent({ questions, responseData, onRetry }: ResponseErrorComponentProps) {
|
||||
return (
|
||||
<div className="fb-flex fb-flex-col fb-bg-white fb-p-4">
|
||||
<span className="fb-mb-1.5 fb-text-base fb-font-bold fb-leading-6 fb-text-slate-900">
|
||||
Your feedback is stuck :(
|
||||
</span>
|
||||
<p className="fb-max-w-md fb-text-sm fb-font-normal fb-leading-6 fb-text-slate-600">
|
||||
The servers cannot be reached at the moment.
|
||||
<br />
|
||||
Please retry now or try again later.
|
||||
</p>
|
||||
<div className="fb-mt-4 fb-rounded-lg fb-border fb-border-slate-200 fb-bg-slate-100 fb-px-4 fb-py-5">
|
||||
<div className="fb-flex fb-max-h-36 fb-flex-1 fb-flex-col fb-space-y-3 fb-overflow-y-scroll">
|
||||
{questions.map((question, index) => {
|
||||
const response = responseData[question.id];
|
||||
if (!response) return;
|
||||
return (
|
||||
<div className="fb-flex fb-flex-col" key={`response-${index.toString()}`}>
|
||||
<span className="fb-text-sm fb-leading-6 fb-text-slate-900">{`Question ${(index + 1).toString()}`}</span>
|
||||
<span className="fb-mt-1 fb-text-sm fb-font-semibold fb-leading-6 fb-text-slate-900">
|
||||
{processResponseData(response)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="fb-mt-4 fb-flex fb-flex-1 fb-flex-row fb-items-center fb-justify-end fb-space-x-2">
|
||||
<SubmitButton
|
||||
tabIndex={2}
|
||||
buttonLabel="Retry"
|
||||
isLastQuestion={false}
|
||||
onClick={() => {
|
||||
onRetry?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { JSX } from "preact";
|
||||
import { FunctionComponent } from "preact";
|
||||
import type { FunctionComponent, JSX } from "preact";
|
||||
|
||||
export const TiredFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
|
||||
return (
|
||||
@@ -468,4 +467,4 @@ export const GrinningSquintingFace: FunctionComponent<JSX.HTMLAttributes<SVGCirc
|
||||
);
|
||||
};
|
||||
|
||||
export let icons = [<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg"></svg>];
|
||||
export const icons = [<svg key="smiley" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg" />];
|
||||
@@ -1,11 +1,11 @@
|
||||
import { TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface SubheaderProps {
|
||||
subheader?: string;
|
||||
questionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const Subheader = ({ subheader, questionId }: SubheaderProps) => {
|
||||
export function Subheader({ subheader, questionId }: SubheaderProps) {
|
||||
return (
|
||||
<p
|
||||
htmlFor={questionId}
|
||||
@@ -14,4 +14,4 @@ export const Subheader = ({ subheader, questionId }: SubheaderProps) => {
|
||||
{subheader}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
interface SurveyCloseButtonProps {
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const SurveyCloseButton = ({ onClose }: SurveyCloseButtonProps) => {
|
||||
export function SurveyCloseButton({ onClose }: SurveyCloseButtonProps) {
|
||||
return (
|
||||
<div class="fb-z-[1001] fb-flex fb-w-fit fb-items-center even:fb-border-l even:fb-pl-1">
|
||||
<div className="fb-z-[1001] fb-flex fb-w-fit fb-items-center even:fb-border-l even:fb-pl-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
class="fb-text-heading fb-relative fb-h-5 fb-w-5 fb-rounded-md hover:fb-bg-black/5 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
className="fb-text-heading fb-relative fb-h-5 fb-w-5 fb-rounded-md hover:fb-bg-black/5 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
|
||||
<svg
|
||||
class="fb-h-5 fb-w-5 fb-p-0.5"
|
||||
className="fb-h-5 fb-w-5 fb-p-0.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1"
|
||||
@@ -21,4 +21,4 @@ export const SurveyCloseButton = ({ onClose }: SurveyCloseButtonProps) => {
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SurveyInlineProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { Survey } from "./Survey";
|
||||
import { type SurveyInlineProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { Survey } from "./survey";
|
||||
|
||||
export const SurveyInline = (props: SurveyInlineProps) => {
|
||||
export function SurveyInline(props: SurveyInlineProps) {
|
||||
return (
|
||||
<div
|
||||
id="fbjs"
|
||||
@@ -13,4 +13,4 @@ export const SurveyInline = (props: SurveyInlineProps) => {
|
||||
<Survey {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Modal } from "@/components/wrappers/Modal";
|
||||
import { Modal } from "@/components/wrappers/modal";
|
||||
import { useState } from "preact/hooks";
|
||||
import { SurveyModalProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { Survey } from "./Survey";
|
||||
import { type SurveyModalProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { Survey } from "./survey";
|
||||
|
||||
export const SurveyModal = ({
|
||||
export function SurveyModal({
|
||||
survey,
|
||||
isBrandingEnabled,
|
||||
getSetIsError,
|
||||
@@ -14,7 +14,7 @@ export const SurveyModal = ({
|
||||
getSetIsResponseSendingFinished,
|
||||
onResponse,
|
||||
onClose,
|
||||
onFinished = () => {},
|
||||
onFinished,
|
||||
onFileUpload,
|
||||
onRetry,
|
||||
isRedirectDisabled = false,
|
||||
@@ -22,7 +22,7 @@ export const SurveyModal = ({
|
||||
responseCount,
|
||||
styling,
|
||||
hiddenFieldsRecord,
|
||||
}: SurveyModalProps) => {
|
||||
}: SurveyModalProps) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const close = () => {
|
||||
@@ -34,7 +34,7 @@ export const SurveyModal = ({
|
||||
}, 1000); // wait for animation to finish}
|
||||
};
|
||||
|
||||
const highlightBorderColor = styling?.highlightBorderColor?.light || null;
|
||||
const highlightBorderColor = styling.highlightBorderColor?.light ?? null;
|
||||
|
||||
return (
|
||||
<div id="fbjs" className="fb-formbricks-form">
|
||||
@@ -54,11 +54,11 @@ export const SurveyModal = ({
|
||||
languageCode={languageCode}
|
||||
onClose={close}
|
||||
onFinished={() => {
|
||||
onFinished();
|
||||
onFinished?.();
|
||||
setTimeout(
|
||||
() => {
|
||||
const firstEnabledEnding = survey.endings[0];
|
||||
if (firstEnabledEnding?.type !== "redirectToUrl") {
|
||||
if (firstEnabledEnding.type !== "redirectToUrl") {
|
||||
close();
|
||||
}
|
||||
},
|
||||
@@ -78,4 +78,4 @@ export const SurveyModal = ({
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
import { EndingCard } from "@/components/general/EndingCard";
|
||||
import { FormbricksBranding } from "@/components/general/FormbricksBranding";
|
||||
import { LanguageSwitch } from "@/components/general/LanguageSwitch";
|
||||
import { ProgressBar } from "@/components/general/ProgressBar";
|
||||
import { QuestionConditional } from "@/components/general/QuestionConditional";
|
||||
import { ResponseErrorComponent } from "@/components/general/ResponseErrorComponent";
|
||||
import { SurveyCloseButton } from "@/components/general/SurveyCloseButton";
|
||||
import { WelcomeCard } from "@/components/general/WelcomeCard";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
|
||||
import { StackedCardsContainer } from "@/components/wrappers/StackedCardsContainer";
|
||||
import { EndingCard } from "@/components/general/ending-card";
|
||||
import { FormbricksBranding } from "@/components/general/formbricks-branding";
|
||||
import { LanguageSwitch } from "@/components/general/language-switch";
|
||||
import { ProgressBar } from "@/components/general/progress-bar";
|
||||
import { QuestionConditional } from "@/components/general/question-conditional";
|
||||
import { ResponseErrorComponent } from "@/components/general/response-error-component";
|
||||
import { SurveyCloseButton } from "@/components/general/survey-close-button";
|
||||
import { WelcomeCard } from "@/components/general/welcome-card";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/auto-close-wrapper";
|
||||
import { StackedCardsContainer } from "@/components/wrappers/stacked-cards-container";
|
||||
import { parseRecallInformation } from "@/lib/recall";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { evaluateLogic, performActions } from "@formbricks/lib/surveyLogic/utils";
|
||||
import { SurveyBaseProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type SurveyBaseProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import type {
|
||||
TResponseData,
|
||||
TResponseDataValue,
|
||||
TResponseTtc,
|
||||
TResponseVariables,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface VariableStackEntry {
|
||||
questionId: TSurveyQuestionId;
|
||||
variables: TResponseVariables;
|
||||
}
|
||||
|
||||
export const Survey = ({
|
||||
export function Survey({
|
||||
survey,
|
||||
styling,
|
||||
isBrandingEnabled,
|
||||
onDisplay = () => {},
|
||||
onResponse = () => {},
|
||||
onClose = () => {},
|
||||
onFinished = () => {},
|
||||
onRetry = () => {},
|
||||
onDisplay,
|
||||
onResponse,
|
||||
onClose,
|
||||
onFinished,
|
||||
onRetry,
|
||||
isRedirectDisabled = false,
|
||||
prefillResponseData,
|
||||
skipPrefilled,
|
||||
@@ -53,7 +53,7 @@ export const Survey = ({
|
||||
shouldResetQuestionId,
|
||||
fullSizeCards = false,
|
||||
autoFocus,
|
||||
}: SurveyBaseProps) => {
|
||||
}: SurveyBaseProps) {
|
||||
const [localSurvey, setlocalSurvey] = useState<TJsEnvironmentStateSurvey>(survey);
|
||||
|
||||
// Update localSurvey when the survey prop changes (it changes in case of survey editor)
|
||||
@@ -61,21 +61,20 @@ export const Survey = ({
|
||||
setlocalSurvey(survey);
|
||||
}, [survey]);
|
||||
|
||||
const autoFocusEnabled = autoFocus !== undefined ? autoFocus : window.self === window.top;
|
||||
const autoFocusEnabled = autoFocus ?? window.self === window.top;
|
||||
|
||||
const [questionId, setQuestionId] = useState(() => {
|
||||
if (startAtQuestionId) {
|
||||
return startAtQuestionId;
|
||||
} else if (localSurvey.welcomeCard.enabled) {
|
||||
return "start";
|
||||
} else {
|
||||
return localSurvey?.questions[0]?.id;
|
||||
}
|
||||
return localSurvey.questions[0]?.id;
|
||||
});
|
||||
const [showError, setShowError] = useState(false);
|
||||
// flag state to store whether response processing has been completed or not, we ignore this check for survey editor preview and link survey preview where getSetIsResponseSendingFinished is undefined
|
||||
const [isResponseSendingFinished, setIsResponseSendingFinished] = useState(
|
||||
getSetIsResponseSendingFinished ? false : true
|
||||
!getSetIsResponseSendingFinished
|
||||
);
|
||||
const [selectedLanguage, setselectedLanguage] = useState(languageCode);
|
||||
const [loadingElement, setLoadingElement] = useState(false);
|
||||
@@ -83,10 +82,10 @@ export const Survey = ({
|
||||
const [responseData, setResponseData] = useState<TResponseData>(hiddenFieldsRecord ?? {});
|
||||
const [_variableStack, setVariableStack] = useState<VariableStackEntry[]>([]);
|
||||
const [currentVariables, setCurrentVariables] = useState<TResponseVariables>(() => {
|
||||
return localSurvey.variables.reduce((acc, variable) => {
|
||||
return localSurvey.variables.reduce<TResponseVariables>((acc, variable) => {
|
||||
acc[variable.id] = variable.value;
|
||||
return acc;
|
||||
}, {} as TResponseVariables);
|
||||
}, {});
|
||||
});
|
||||
|
||||
const [ttc, setTtc] = useState<TResponseTtc>({});
|
||||
@@ -97,9 +96,8 @@ export const Survey = ({
|
||||
const cardArrangement = useMemo(() => {
|
||||
if (localSurvey.type === "link") {
|
||||
return styling.cardArrangement?.linkSurveys ?? "straight";
|
||||
} else {
|
||||
return styling.cardArrangement?.appSurveys ?? "straight";
|
||||
}
|
||||
return styling.cardArrangement?.appSurveys ?? "straight";
|
||||
}, [localSurvey.type, styling.cardArrangement?.linkSurveys, styling.cardArrangement?.appSurveys]);
|
||||
|
||||
const currentQuestionIndex = localSurvey.questions.findIndex((q) => q.id === questionId);
|
||||
@@ -108,15 +106,14 @@ export const Survey = ({
|
||||
const newHistory = [...history];
|
||||
const prevQuestionId = newHistory.pop();
|
||||
return localSurvey.questions.find((q) => q.id === prevQuestionId);
|
||||
} else {
|
||||
return localSurvey.questions.find((q) => q.id === questionId);
|
||||
}
|
||||
return localSurvey.questions.find((q) => q.id === questionId);
|
||||
}, [questionId, localSurvey, history]);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const showProgressBar = !styling.hideProgressBar;
|
||||
const getShowSurveyCloseButton = (offset: number) => {
|
||||
return offset === 0 && localSurvey.type !== "link" && (clickOutside === undefined ? true : clickOutside);
|
||||
return offset === 0 && localSurvey.type !== "link" && (clickOutside ?? true);
|
||||
};
|
||||
const getShowLanguageSwitch = (offset: number) => {
|
||||
return localSurvey.showLanguageSwitch && localSurvey.languages.length > 0 && offset <= 0;
|
||||
@@ -131,9 +128,9 @@ export const Survey = ({
|
||||
|
||||
useEffect(() => {
|
||||
// call onDisplay when component is mounted
|
||||
onDisplay();
|
||||
onDisplay?.();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- onDisplay should only be called once
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -172,9 +169,6 @@ export const Survey = ({
|
||||
setselectedLanguage(languageCode);
|
||||
}, [languageCode]);
|
||||
|
||||
let currIdxTemp = currentQuestionIndex;
|
||||
let currQuesTemp = currentQuestion;
|
||||
|
||||
const onChange = (responseDataUpdate: TResponseData) => {
|
||||
const updatedResponseData = { ...responseData, ...responseDataUpdate };
|
||||
setResponseData(updatedResponseData);
|
||||
@@ -185,11 +179,11 @@ export const Survey = ({
|
||||
setCurrentVariables(updatedVariables);
|
||||
};
|
||||
|
||||
const makeQuestionsRequired = (questionIds: string[]): void => {
|
||||
const makeQuestionsRequired = (requiredQuestionIds: string[]): void => {
|
||||
setlocalSurvey((prevSurvey) => ({
|
||||
...prevSurvey,
|
||||
questions: prevSurvey.questions.map((question) => {
|
||||
if (questionIds.includes(question.id)) {
|
||||
if (requiredQuestionIds.includes(question.id)) {
|
||||
return {
|
||||
...question,
|
||||
required: true,
|
||||
@@ -200,8 +194,11 @@ export const Survey = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const pushVariableState = (questionId: TSurveyQuestionId) => {
|
||||
setVariableStack((prevStack) => [...prevStack, { questionId, variables: { ...currentVariables } }]);
|
||||
const pushVariableState = (currentQuestionId: TSurveyQuestionId) => {
|
||||
setVariableStack((prevStack) => [
|
||||
...prevStack,
|
||||
{ questionId: currentQuestionId, variables: { ...currentVariables } },
|
||||
]);
|
||||
};
|
||||
|
||||
const popVariableState = () => {
|
||||
@@ -224,7 +221,7 @@ export const Survey = ({
|
||||
if (questionId === "start")
|
||||
return { nextQuestionId: questions[0]?.id || firstEndingId, calculatedVariables: {} };
|
||||
|
||||
if (!currQuesTemp) throw new Error("Question not found");
|
||||
if (!currentQuestion) throw new Error("Question not found");
|
||||
|
||||
let firstJumpTarget: string | undefined;
|
||||
const allRequiredQuestionIds: string[] = [];
|
||||
@@ -232,8 +229,8 @@ export const Survey = ({
|
||||
let calculationResults = { ...currentVariables };
|
||||
const localResponseData = { ...responseData, ...data };
|
||||
|
||||
if (currQuesTemp.logic && currQuesTemp.logic.length > 0) {
|
||||
for (const logic of currQuesTemp.logic) {
|
||||
if (currentQuestion.logic && currentQuestion.logic.length > 0) {
|
||||
for (const logic of currentQuestion.logic) {
|
||||
if (
|
||||
evaluateLogic(
|
||||
localSurvey,
|
||||
@@ -261,8 +258,8 @@ export const Survey = ({
|
||||
}
|
||||
|
||||
// Use logicFallback if no jump target was set
|
||||
if (!firstJumpTarget && currQuesTemp.logicFallback) {
|
||||
firstJumpTarget = currQuesTemp.logicFallback;
|
||||
if (!firstJumpTarget && currentQuestion.logicFallback) {
|
||||
firstJumpTarget = currentQuestion.logicFallback;
|
||||
}
|
||||
|
||||
// Make all collected questions required
|
||||
@@ -271,18 +268,18 @@ export const Survey = ({
|
||||
}
|
||||
|
||||
// Return the first jump target if found, otherwise go to the next question or ending
|
||||
const nextQuestionId = firstJumpTarget || questions[currentQuestionIndex + 1]?.id || firstEndingId;
|
||||
const nextQuestionId = firstJumpTarget ?? questions[currentQuestionIndex + 1]?.id ?? firstEndingId;
|
||||
|
||||
return { nextQuestionId, calculatedVariables: calculationResults };
|
||||
};
|
||||
|
||||
const onSubmit = (responseData: TResponseData, ttc: TResponseTtc) => {
|
||||
const questionId = Object.keys(responseData)[0];
|
||||
const onSubmit = (surveyResponseData: TResponseData, responsettc: TResponseTtc) => {
|
||||
const respondedQuestionId = Object.keys(surveyResponseData)[0];
|
||||
setLoadingElement(true);
|
||||
|
||||
pushVariableState(questionId);
|
||||
pushVariableState(respondedQuestionId);
|
||||
|
||||
const { nextQuestionId, calculatedVariables } = evaluateLogicAndGetNextQuestionId(responseData);
|
||||
const { nextQuestionId, calculatedVariables } = evaluateLogicAndGetNextQuestionId(surveyResponseData);
|
||||
const finished =
|
||||
nextQuestionId === undefined ||
|
||||
!localSurvey.questions.map((question) => question.id).includes(nextQuestionId);
|
||||
@@ -291,11 +288,11 @@ export const Survey = ({
|
||||
? localSurvey.endings.find((ending) => ending.id === nextQuestionId)?.id
|
||||
: undefined;
|
||||
|
||||
onChange(responseData);
|
||||
onChange(surveyResponseData);
|
||||
onChangeVariables(calculatedVariables);
|
||||
onResponse({
|
||||
data: responseData,
|
||||
ttc,
|
||||
onResponse?.({
|
||||
data: surveyResponseData,
|
||||
ttc: responsettc,
|
||||
finished,
|
||||
variables: calculatedVariables,
|
||||
language: selectedLanguage,
|
||||
@@ -304,26 +301,26 @@ export const Survey = ({
|
||||
if (finished) {
|
||||
// Post a message to the parent window indicating that the survey is completed.
|
||||
window.parent.postMessage("formbricksSurveyCompleted", "*");
|
||||
onFinished();
|
||||
onFinished?.();
|
||||
}
|
||||
if (nextQuestionId) {
|
||||
setQuestionId(nextQuestionId);
|
||||
}
|
||||
// add to history
|
||||
setHistory([...history, questionId]);
|
||||
setHistory([...history, respondedQuestionId]);
|
||||
setLoadingElement(false);
|
||||
};
|
||||
|
||||
const onBack = (): void => {
|
||||
let prevQuestionId;
|
||||
// use history if available
|
||||
if (history?.length > 0) {
|
||||
if (history.length > 0) {
|
||||
const newHistory = [...history];
|
||||
prevQuestionId = newHistory.pop();
|
||||
setHistory(newHistory);
|
||||
} else {
|
||||
// otherwise go back to previous question in array
|
||||
prevQuestionId = localSurvey.questions[currIdxTemp - 1]?.id;
|
||||
prevQuestionId = localSurvey.questions[currentQuestionIndex - 1]?.id;
|
||||
}
|
||||
popVariableState();
|
||||
if (!prevQuestionId) throw new Error("Question not found");
|
||||
@@ -331,11 +328,11 @@ export const Survey = ({
|
||||
};
|
||||
|
||||
const getQuestionPrefillData = (
|
||||
questionId: TSurveyQuestionId,
|
||||
prefillQuestionId: TSurveyQuestionId,
|
||||
offset: number
|
||||
): TResponseDataValue | undefined => {
|
||||
if (offset === 0 && prefillResponseData) {
|
||||
return prefillResponseData[questionId];
|
||||
return prefillResponseData[prefillQuestionId];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -392,7 +389,7 @@ export const Survey = ({
|
||||
} else {
|
||||
const question = localSurvey.questions[questionIdx];
|
||||
return (
|
||||
question && (
|
||||
Boolean(question) && (
|
||||
<QuestionConditional
|
||||
key={question.id}
|
||||
surveyId={localSurvey.id}
|
||||
@@ -404,7 +401,7 @@ export const Survey = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
onFileUpload={onFileUpload}
|
||||
isFirstQuestion={question.id === localSurvey?.questions[0]?.id}
|
||||
isFirstQuestion={question.id === localSurvey.questions[0]?.id}
|
||||
skipPrefilled={skipPrefilled}
|
||||
prefilledQuestionValue={getQuestionPrefillData(question.id, offset)}
|
||||
isLastQuestion={question.id === localSurvey.questions[localSurvey.questions.length - 1].id}
|
||||
@@ -443,8 +440,8 @@ export const Survey = ({
|
||||
{content()}
|
||||
</div>
|
||||
<div className="fb-mx-6 fb-mb-10 fb-mt-2 fb-space-y-3 sm:fb-mb-6 sm:fb-mt-6">
|
||||
{isBrandingEnabled && <FormbricksBranding />}
|
||||
{showProgressBar && <ProgressBar survey={localSurvey} questionId={questionId} />}
|
||||
{isBrandingEnabled ? <FormbricksBranding /> : null}
|
||||
{showProgressBar ? <ProgressBar survey={localSurvey} questionId={questionId} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</AutoCloseWrapper>
|
||||
@@ -452,17 +449,15 @@ export const Survey = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StackedCardsContainer
|
||||
cardArrangement={cardArrangement}
|
||||
currentQuestionId={questionId}
|
||||
getCardContent={getCardContent}
|
||||
survey={localSurvey}
|
||||
styling={styling}
|
||||
setQuestionId={setQuestionId}
|
||||
shouldResetQuestionId={shouldResetQuestionId}
|
||||
fullSizeCards={fullSizeCards}
|
||||
/>
|
||||
</>
|
||||
<StackedCardsContainer
|
||||
cardArrangement={cardArrangement}
|
||||
currentQuestionId={questionId}
|
||||
getCardContent={getCardContent}
|
||||
survey={localSurvey}
|
||||
styling={styling}
|
||||
setQuestionId={setQuestionId}
|
||||
shouldResetQuestionId={shouldResetQuestionId}
|
||||
fullSizeCards={fullSizeCards}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { replaceRecallInfo } from "@/lib/recall";
|
||||
import { calculateElementIdx } from "@/lib/utils";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseTtc, TResponseVariables } from "@formbricks/types/responses";
|
||||
import { TI18nString } from "@formbricks/types/surveys/types";
|
||||
import { Headline } from "./Headline";
|
||||
import { HtmlBody } from "./HtmlBody";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TResponseData, type TResponseTtc, type TResponseVariables } from "@formbricks/types/responses";
|
||||
import { type TI18nString } from "@formbricks/types/surveys/types";
|
||||
import { Headline } from "./headline";
|
||||
import { HtmlBody } from "./html-body";
|
||||
|
||||
interface WelcomeCardProps {
|
||||
headline?: TI18nString;
|
||||
@@ -25,7 +25,7 @@ interface WelcomeCardProps {
|
||||
variablesData: TResponseVariables;
|
||||
}
|
||||
|
||||
const TimerIcon = () => {
|
||||
function TimerIcon() {
|
||||
return (
|
||||
<div className="fb-mr-1">
|
||||
<svg
|
||||
@@ -33,16 +33,16 @@ const TimerIcon = () => {
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-stopwatch"
|
||||
className="bi bi-stopwatch"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8.5 5.6a.5.5 0 1 0-1 0v2.9h-3a.5.5 0 0 0 0 1H8a.5.5 0 0 0 .5-.5V5.6z" />
|
||||
<path d="M6.5 1A.5.5 0 0 1 7 .5h2a.5.5 0 0 1 0 1v.57c1.36.196 2.594.78 3.584 1.64a.715.715 0 0 1 .012-.013l.354-.354-.354-.353a.5.5 0 0 1 .707-.708l1.414 1.415a.5.5 0 1 1-.707.707l-.353-.354-.354.354a.512.512 0 0 1-.013.012A7 7 0 1 1 7 2.071V1.5a.5.5 0 0 1-.5-.5zM8 3a6 6 0 1 0 .001 12A6 6 0 0 0 8 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const UsersIcon = () => {
|
||||
function UsersIcon() {
|
||||
return (
|
||||
<div className="fb-mr-1">
|
||||
<svg
|
||||
@@ -51,7 +51,7 @@ const UsersIcon = () => {
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
class="fb-h-4 fb-w-4">
|
||||
className="fb-h-4 fb-w-4">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -60,9 +60,9 @@ const UsersIcon = () => {
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const WelcomeCard = ({
|
||||
export function WelcomeCard({
|
||||
headline,
|
||||
html,
|
||||
fileUrl,
|
||||
@@ -75,7 +75,7 @@ export const WelcomeCard = ({
|
||||
isCurrent,
|
||||
responseData,
|
||||
variablesData,
|
||||
}: WelcomeCardProps) => {
|
||||
}: WelcomeCardProps) {
|
||||
const calculateTimeToComplete = () => {
|
||||
let idx = calculateElementIdx(survey, 0);
|
||||
if (idx === 0.5) {
|
||||
@@ -95,20 +95,19 @@ export const WelcomeCard = ({
|
||||
if (minutes === 0) {
|
||||
// If less than 1 minute, return 'less than 1 minute'
|
||||
return "less than 1 minute";
|
||||
} else {
|
||||
// If more than 1 minute, return 'less than X minutes', where X is minutes + 1
|
||||
return `less than ${minutes + 1} minutes`;
|
||||
}
|
||||
// If more than 1 minute, return 'less than X minutes', where X is minutes + 1
|
||||
return `less than ${(minutes + 1).toString()} minutes`;
|
||||
}
|
||||
// If there are no remaining seconds, just return the number of minutes
|
||||
return `${minutes} minutes`;
|
||||
return `${minutes.toString()} minutes`;
|
||||
};
|
||||
|
||||
const timeToFinish = survey.welcomeCard.timeToFinish;
|
||||
const showResponseCount = survey.welcomeCard.showResponseCount;
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({ ["welcomeCard"]: "clicked" }, {});
|
||||
onSubmit({ welcomeCard: "clicked" }, {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -128,20 +127,20 @@ export const WelcomeCard = ({
|
||||
document.removeEventListener("keydown", handleEnter);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- only want to run this effect when isCurrent changes
|
||||
}, [isCurrent]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{fileUrl && (
|
||||
{fileUrl ? (
|
||||
<img
|
||||
src={fileUrl}
|
||||
className="fb-mb-8 fb-max-h-96 fb-w-1/3 fb-rounded-lg fb-object-contain"
|
||||
alt="Company Logo"
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<Headline
|
||||
headline={replaceRecallInfo(
|
||||
@@ -157,7 +156,6 @@ export const WelcomeCard = ({
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="fb-mx-6 fb-mt-4 fb-flex fb-gap-4 fb-py-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
@@ -166,10 +164,13 @@ export const WelcomeCard = ({
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
onKeyDown={(e) => e.key === "Enter" && e.preventDefault()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{timeToFinish && !showResponseCount ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
||||
<TimerIcon />
|
||||
@@ -177,22 +178,26 @@ export const WelcomeCard = ({
|
||||
<span> Takes {calculateTimeToComplete()} </span>
|
||||
</p>
|
||||
</div>
|
||||
) : showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
||||
) : null}
|
||||
{showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
||||
<UsersIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span>{`${responseCount} people responded`}</span>
|
||||
<span>{`${responseCount.toString()} people responded`}</span>
|
||||
</p>
|
||||
</div>
|
||||
) : timeToFinish && showResponseCount ? (
|
||||
) : null}
|
||||
{timeToFinish && showResponseCount ? (
|
||||
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
|
||||
<TimerIcon />
|
||||
<p className="fb-pt-1 fb-text-xs">
|
||||
<span> Takes {calculateTimeToComplete()} </span>
|
||||
<span>{responseCount && responseCount > 3 ? `⋅ ${responseCount} people responded` : ""}</span>
|
||||
<span>
|
||||
{responseCount && responseCount > 3 ? `⋅ ${responseCount.toString()} people responded` : ""}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { Input } from "@/components/general/Input";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyAddressQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface AddressQuestionProps {
|
||||
@@ -27,7 +27,7 @@ interface AddressQuestionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
}
|
||||
|
||||
export const AddressQuestion = ({
|
||||
export function AddressQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -40,7 +40,7 @@ export const AddressQuestion = ({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
}: AddressQuestionProps) => {
|
||||
}: AddressQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
@@ -88,7 +88,7 @@ export const AddressQuestion = ({
|
||||
if (field.id === fieldId) {
|
||||
return fieldValue;
|
||||
}
|
||||
const existingValue = safeValue?.[fields.findIndex((f) => f.id === field.id)] || "";
|
||||
const existingValue = safeValue[fields.findIndex((f) => f.id === field.id)] || "";
|
||||
return field.show ? existingValue : "";
|
||||
});
|
||||
onChange({ [question.id]: newValue });
|
||||
@@ -98,11 +98,11 @@ export const AddressQuestion = ({
|
||||
e.preventDefault();
|
||||
const updatedTtc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtc);
|
||||
const containsAllEmptyStrings = safeValue?.length === 6 && safeValue.every((item) => item.trim() === "");
|
||||
const containsAllEmptyStrings = safeValue.length === 6 && safeValue.every((item) => item.trim() === "");
|
||||
if (containsAllEmptyStrings) {
|
||||
onSubmit({ [question.id]: [] }, updatedTtc);
|
||||
} else {
|
||||
onSubmit({ [question.id]: safeValue ?? [] }, updatedTtc);
|
||||
onSubmit({ [question.id]: safeValue }, updatedTtc);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,7 +120,9 @@ export const AddressQuestion = ({
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -131,7 +133,7 @@ export const AddressQuestion = ({
|
||||
questionId={question.id}
|
||||
/>
|
||||
|
||||
<div className={`fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full`}>
|
||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
||||
{fields.map((field, index) => {
|
||||
const isFieldRequired = () => {
|
||||
if (field.required) {
|
||||
@@ -140,7 +142,7 @@ export const AddressQuestion = ({
|
||||
|
||||
// if all fields are optional and the question is required, then the fields should be required
|
||||
if (
|
||||
fields.filter((field) => field.show).every((field) => !field.required) &&
|
||||
fields.filter((currField) => currField.show).every((currField) => !currField.required) &&
|
||||
question.required
|
||||
) {
|
||||
return true;
|
||||
@@ -155,10 +157,12 @@ export const AddressQuestion = ({
|
||||
key={field.id}
|
||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue?.[index] || ""}
|
||||
value={safeValue[index] || ""}
|
||||
className="fb-py-3"
|
||||
type={field.id === "email" ? "email" : "text"}
|
||||
onChange={(e) => handleChange(field.id, e?.currentTarget?.value ?? "")}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
ref={index === 0 ? addressRef : null}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
@@ -173,9 +177,8 @@ export const AddressQuestion = ({
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<div></div>
|
||||
<div />
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
@@ -190,4 +193,4 @@ export const AddressQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { CalEmbed } from "@/components/general/CalEmbed";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { CalEmbed } from "@/components/general/cal-embed";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyCalQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TSurveyCalQuestion, type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface CalQuestionProps {
|
||||
question: TSurveyCalQuestion;
|
||||
@@ -27,7 +26,7 @@ interface CalQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const CalQuestion = ({
|
||||
export function CalQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -39,7 +38,7 @@ export const CalQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: CalQuestionProps) => {
|
||||
}: CalQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
@@ -71,7 +70,9 @@ export const CalQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -81,10 +82,8 @@ export const CalQuestion = ({
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<>
|
||||
{errorMessage && <span className="fb-text-red-500">{errorMessage}</span>}
|
||||
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
|
||||
</>
|
||||
{errorMessage ? <span className="fb-text-red-500">{errorMessage}</span> : null}
|
||||
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div className="fb-flex fb-flex-row-reverse fb-w-full fb-justify-between fb-px-6 fb-py-4">
|
||||
@@ -95,7 +94,7 @@ export const CalQuestion = ({
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
)}
|
||||
<div></div>
|
||||
<div />
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
|
||||
@@ -108,4 +107,4 @@ export const CalQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { HtmlBody } from "@/components/general/HtmlBody";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { HtmlBody } from "@/components/general/html-body";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyConsentQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ConsentQuestionProps {
|
||||
@@ -25,7 +25,7 @@ interface ConsentQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const ConsentQuestion = ({
|
||||
export function ConsentQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -38,7 +38,7 @@ export const ConsentQuestion = ({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
}: ConsentQuestionProps) => {
|
||||
}: ConsentQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
@@ -66,7 +66,9 @@ export const ConsentQuestion = ({
|
||||
}}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -123,7 +125,7 @@ export const ConsentQuestion = ({
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
<div></div>
|
||||
<div />
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
@@ -139,4 +141,4 @@ export const ConsentQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { Input } from "@/components/general/Input";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { Input } from "@/components/general/input";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyContactInfoQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ContactInfoQuestionProps {
|
||||
@@ -27,7 +27,7 @@ interface ContactInfoQuestionProps {
|
||||
autoFocusEnabled: boolean;
|
||||
}
|
||||
|
||||
export const ContactInfoQuestion = ({
|
||||
export function ContactInfoQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -40,7 +40,7 @@ export const ContactInfoQuestion = ({
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
autoFocusEnabled,
|
||||
}: ContactInfoQuestionProps) => {
|
||||
}: ContactInfoQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
@@ -83,7 +83,7 @@ export const ContactInfoQuestion = ({
|
||||
if (field.id === fieldId) {
|
||||
return fieldValue;
|
||||
}
|
||||
const existingValue = safeValue?.[fields.findIndex((f) => f.id === field.id)] || "";
|
||||
const existingValue = safeValue[fields.findIndex((f) => f.id === field.id)] || "";
|
||||
return field.show ? existingValue : "";
|
||||
});
|
||||
onChange({ [question.id]: newValue });
|
||||
@@ -93,11 +93,11 @@ export const ContactInfoQuestion = ({
|
||||
e.preventDefault();
|
||||
const updatedTtc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtc);
|
||||
const containsAllEmptyStrings = safeValue?.length === 5 && safeValue.every((item) => item.trim() === "");
|
||||
const containsAllEmptyStrings = safeValue.length === 5 && safeValue.every((item) => item.trim() === "");
|
||||
if (containsAllEmptyStrings) {
|
||||
onSubmit({ [question.id]: [] }, updatedTtc);
|
||||
} else {
|
||||
onSubmit({ [question.id]: safeValue ?? [] }, updatedTtc);
|
||||
onSubmit({ [question.id]: safeValue }, updatedTtc);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,7 +115,9 @@ export const ContactInfoQuestion = ({
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -126,7 +128,7 @@ export const ContactInfoQuestion = ({
|
||||
questionId={question.id}
|
||||
/>
|
||||
|
||||
<div className={`fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full`}>
|
||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
||||
{fields.map((field, index) => {
|
||||
const isFieldRequired = () => {
|
||||
if (field.required) {
|
||||
@@ -135,7 +137,7 @@ export const ContactInfoQuestion = ({
|
||||
|
||||
// if all fields are optional and the question is required, then the fields should be required
|
||||
if (
|
||||
fields.filter((field) => field.show).every((field) => !field.required) &&
|
||||
fields.filter((currField) => currField.show).every((currField) => !currField.required) &&
|
||||
question.required
|
||||
) {
|
||||
return true;
|
||||
@@ -158,10 +160,12 @@ export const ContactInfoQuestion = ({
|
||||
key={field.id}
|
||||
placeholder={isFieldRequired() ? `${field.placeholder}*` : field.placeholder}
|
||||
required={isFieldRequired()}
|
||||
value={safeValue?.[index] || ""}
|
||||
value={safeValue[index] || ""}
|
||||
className="fb-py-3"
|
||||
type={inputType}
|
||||
onChange={(e) => handleChange(field.id, e?.currentTarget?.value ?? "")}
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
)
|
||||
@@ -176,7 +180,6 @@ export const ContactInfoQuestion = ({
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
@@ -192,4 +195,4 @@ export const ContactInfoQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { HtmlBody } from "@/components/general/HtmlBody";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { HtmlBody } from "@/components/general/html-body";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyCTAQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface CTAQuestionProps {
|
||||
@@ -26,7 +25,7 @@ interface CTAQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const CTAQuestion = ({
|
||||
export function CTAQuestion({
|
||||
question,
|
||||
onSubmit,
|
||||
onChange,
|
||||
@@ -38,7 +37,7 @@ export const CTAQuestion = ({
|
||||
setTtc,
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
}: CTAQuestionProps) => {
|
||||
}: CTAQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
@@ -48,7 +47,9 @@ export const CTAQuestion = ({
|
||||
<div key={question.id}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -66,7 +67,7 @@ export const CTAQuestion = ({
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onClick={() => {
|
||||
if (question.buttonExternal && question.buttonUrl) {
|
||||
window?.open(question.buttonUrl, "_blank")?.focus();
|
||||
window.open(question.buttonUrl, "_blank")?.focus();
|
||||
}
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
@@ -106,4 +107,4 @@ export const CTAQuestion = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import DatePicker from "react-date-picker";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getMonthName, getOrdinalDate } from "@formbricks/lib/utils/datetime";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import "../../styles/date-picker.css";
|
||||
|
||||
@@ -30,52 +30,56 @@ interface DateQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
const CalendarIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
class="lucide lucide-calendar-days">
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="M8 14h.01" />
|
||||
<path d="M12 14h.01" />
|
||||
<path d="M16 14h.01" />
|
||||
<path d="M8 18h.01" />
|
||||
<path d="M12 18h.01" />
|
||||
<path d="M16 18h.01" />
|
||||
</svg>
|
||||
);
|
||||
function CalendarIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-calendar-days">
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="M8 14h.01" />
|
||||
<path d="M12 14h.01" />
|
||||
<path d="M16 14h.01" />
|
||||
<path d="M8 18h.01" />
|
||||
<path d="M12 18h.01" />
|
||||
<path d="M16 18h.01" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const CalendarCheckIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
class="lucide lucide-calendar-check">
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="m9 16 2 2 4-4" />
|
||||
</svg>
|
||||
);
|
||||
function CalendarCheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-calendar-check">
|
||||
<path d="M8 2v4" />
|
||||
<path d="M16 2v4" />
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="m9 16 2 2 4-4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const DateQuestion = ({
|
||||
export function DateQuestion({
|
||||
question,
|
||||
value,
|
||||
onSubmit,
|
||||
@@ -87,7 +91,7 @@ export const DateQuestion = ({
|
||||
setTtc,
|
||||
ttc,
|
||||
currentQuestionId,
|
||||
}: DateQuestionProps) => {
|
||||
}: DateQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -100,7 +104,7 @@ export const DateQuestion = ({
|
||||
useEffect(() => {
|
||||
if (datePickerOpen) {
|
||||
if (!selectedDate) setSelectedDate(new Date());
|
||||
const input = document.querySelector(".react-date-picker__inputGroup__input") as HTMLInputElement;
|
||||
const input: HTMLInputElement = document.querySelector(".react-date-picker__inputGroup__input")!;
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
@@ -108,7 +112,7 @@ export const DateQuestion = ({
|
||||
}, [datePickerOpen, selectedDate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!selectedDate) {
|
||||
if (selectedDate) {
|
||||
if (hideInvalid) {
|
||||
setHideInvalid(false);
|
||||
}
|
||||
@@ -142,7 +146,9 @@ export const DateQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -152,7 +158,7 @@ export const DateQuestion = ({
|
||||
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
|
||||
questionId={question.id}
|
||||
/>
|
||||
<div className={"fb-text-red-600"}>
|
||||
<div className="fb-text-red-600">
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -161,7 +167,9 @@ export const DateQuestion = ({
|
||||
<div className="fb-relative">
|
||||
{!datePickerOpen && (
|
||||
<div
|
||||
onClick={() => setDatePickerOpen(true)}
|
||||
onClick={() => {
|
||||
setDatePickerOpen(true);
|
||||
}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") setDatePickerOpen(true);
|
||||
@@ -234,9 +242,10 @@ export const DateQuestion = ({
|
||||
}
|
||||
// active date class
|
||||
if (
|
||||
selectedDate &&
|
||||
date.getDate() === selectedDate?.getDate() &&
|
||||
date.getMonth() === selectedDate?.getMonth() &&
|
||||
date.getFullYear() === selectedDate?.getFullYear()
|
||||
date.getMonth() === selectedDate.getMonth() &&
|
||||
date.getFullYear() === selectedDate.getFullYear()
|
||||
) {
|
||||
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading`;
|
||||
}
|
||||
@@ -273,4 +282,4 @@ export const DateQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import type { TSurveyFileUploadQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { BackButton } from "../buttons/BackButton";
|
||||
import { FileInput } from "../general/FileInput";
|
||||
import { Subheader } from "../general/Subheader";
|
||||
import { BackButton } from "../buttons/back-button";
|
||||
import { FileInput } from "../general/file-input";
|
||||
import { Subheader } from "../general/subheader";
|
||||
|
||||
interface FileUploadQuestionProps {
|
||||
question: TSurveyFileUploadQuestion;
|
||||
@@ -30,7 +30,7 @@ interface FileUploadQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const FileUploadQuestion = ({
|
||||
export function FileUploadQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -44,7 +44,7 @@ export const FileUploadQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: FileUploadQuestionProps) => {
|
||||
}: FileUploadQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
@@ -63,18 +63,18 @@ export const FileUploadQuestion = ({
|
||||
} else {
|
||||
alert("Please upload a file");
|
||||
}
|
||||
} else if (value) {
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
} else {
|
||||
if (value) {
|
||||
onSubmit({ [question.id]: value }, updatedTtcObj);
|
||||
} else {
|
||||
onSubmit({ [question.id]: "skipped" }, updatedTtcObj);
|
||||
}
|
||||
onSubmit({ [question.id]: "skipped" }, updatedTtcObj);
|
||||
}
|
||||
}}
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -95,12 +95,12 @@ export const FileUploadQuestion = ({
|
||||
onChange({ [question.id]: "skipped" });
|
||||
}
|
||||
}}
|
||||
fileUrls={value as string[]}
|
||||
fileUrls={value}
|
||||
allowMultipleFiles={question.allowMultipleFiles}
|
||||
{...(!!question.allowedFileExtensions
|
||||
{...(question.allowedFileExtensions
|
||||
? { allowedFileExtensions: question.allowedFileExtensions }
|
||||
: {})}
|
||||
{...(!!question.maxSizeInMB ? { maxSizeInMB: question.maxSizeInMB } : {})}
|
||||
{...(question.maxSizeInMB ? { maxSizeInMB: question.maxSizeInMB } : {})}
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
@@ -122,4 +122,4 @@ export const FileUploadQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { getShuffledRowIndices } from "@/lib/utils";
|
||||
import { JSX } from "preact";
|
||||
import { type JSX } from "preact";
|
||||
import { useCallback, useMemo, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TI18nString, TSurveyMatrixQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface MatrixQuestionProps {
|
||||
@@ -26,7 +26,7 @@ interface MatrixQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const MatrixQuestion = ({
|
||||
export function MatrixQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -38,25 +38,25 @@ export const MatrixQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: MatrixQuestionProps) => {
|
||||
}: MatrixQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
const rowShuffleIdx = useMemo(() => {
|
||||
if (question.shuffleOption) {
|
||||
if (question.shuffleOption !== "none") {
|
||||
return getShuffledRowIndices(question.rows.length, question.shuffleOption);
|
||||
} else {
|
||||
return question.rows.map((_, id) => id);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return question.rows.map((_, id) => id);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to recompute when the shuffleOption changes
|
||||
}, [question.shuffleOption, question.rows.length]);
|
||||
|
||||
const questionRows = useMemo(() => {
|
||||
if (!question.rows) {
|
||||
if (!question.rows.length) {
|
||||
return [];
|
||||
}
|
||||
if (question.shuffleOption === "none" || question.shuffleOption === undefined) {
|
||||
if (question.shuffleOption === "none") {
|
||||
return question.rows;
|
||||
}
|
||||
return rowShuffleIdx.map((shuffledIdx) => {
|
||||
@@ -87,7 +87,7 @@ export const MatrixQuestion = ({
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: JSX.TargetedEvent<HTMLFormElement, Event>) => {
|
||||
(e: JSX.TargetedEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const updatedTtc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtc);
|
||||
@@ -119,7 +119,9 @@ export const MatrixQuestion = ({
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -133,14 +135,14 @@ export const MatrixQuestion = ({
|
||||
<table className="fb-no-scrollbar fb-min-w-full fb-table-auto fb-border-collapse fb-text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="fb-px-4 fb-py-2"></th>
|
||||
<th className="fb-px-4 fb-py-2" />
|
||||
{columnsHeaders}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{questionRows.map((row, rowIndex) => (
|
||||
// Table rows
|
||||
<tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}>
|
||||
<tr className={rowIndex % 2 === 0 ? "bg-input-bg" : ""} key={`row-${rowIndex.toString()}`}>
|
||||
<td
|
||||
className="fb-text-heading fb-rounded-l-custom fb-max-w-40 fb-break-words fb-pr-4 fb-pl-2 fb-py-2"
|
||||
dir="auto">
|
||||
@@ -148,15 +150,15 @@ export const MatrixQuestion = ({
|
||||
</td>
|
||||
{question.columns.map((column, columnIndex) => (
|
||||
<td
|
||||
key={columnIndex}
|
||||
key={`column-${columnIndex.toString()}`}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-slate-800 ${columnIndex === question.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
handleSelect(
|
||||
getLocalizedValue(column, languageCode),
|
||||
getLocalizedValue(row, languageCode)
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
@@ -174,7 +176,7 @@ export const MatrixQuestion = ({
|
||||
type="radio"
|
||||
tabIndex={-1}
|
||||
required={question.required}
|
||||
id={`${row}-${column}`}
|
||||
id={`row${rowIndex.toString()}-column${columnIndex.toString()}`}
|
||||
name={getLocalizedValue(row, languageCode)}
|
||||
value={getLocalizedValue(column, languageCode)}
|
||||
checked={
|
||||
@@ -199,7 +201,6 @@ export const MatrixQuestion = ({
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
isLastQuestion={isLastQuestion}
|
||||
onClick={() => {}}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
/>
|
||||
{!isFirstQuestion && (
|
||||
@@ -212,4 +213,4 @@ export const MatrixQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, getShuffledChoicesIds } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
@@ -26,7 +26,7 @@ interface MultipleChoiceMultiProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const MultipleChoiceMultiQuestion = ({
|
||||
export function MultipleChoiceMultiQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -39,7 +39,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
setTtc,
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
}: MultipleChoiceMultiProps) => {
|
||||
}: MultipleChoiceMultiProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
@@ -47,8 +47,9 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
const shuffledChoicesIds = useMemo(() => {
|
||||
if (question.shuffleOption) {
|
||||
return getShuffledChoicesIds(question.choices, question.shuffleOption);
|
||||
} else return question.choices.map((choice) => choice.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}
|
||||
return question.choices.map((choice) => choice.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to recompute this when the shuffleOption changes
|
||||
}, [question.shuffleOption, question.choices.length, question.choices[question.choices.length - 1].id]);
|
||||
|
||||
const getChoicesWithoutOtherLabels = useCallback(
|
||||
@@ -63,9 +64,9 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
|
||||
useEffect(() => {
|
||||
setOtherSelected(
|
||||
!!value &&
|
||||
((Array.isArray(value) ? value : [value]) as string[]).some((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item) === false;
|
||||
Boolean(value) &&
|
||||
(Array.isArray(value) ? value : [value]).some((item) => {
|
||||
return !getChoicesWithoutOtherLabels().includes(item);
|
||||
})
|
||||
);
|
||||
setOtherValue(
|
||||
@@ -81,8 +82,8 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
}
|
||||
if (question.shuffleOption === "none" || question.shuffleOption === undefined) return question.choices;
|
||||
return shuffledChoicesIds.map((choiceId) => {
|
||||
const choice = question.choices.find((choice) => {
|
||||
return choice.id === choiceId;
|
||||
const choice = question.choices.find((currentChoice) => {
|
||||
return currentChoice.id === choiceId;
|
||||
});
|
||||
return choice;
|
||||
});
|
||||
@@ -115,19 +116,21 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
const newValue = value.filter((v) => {
|
||||
return questionChoiceLabels.includes(v);
|
||||
});
|
||||
return onChange({ [question.id]: [...newValue, item] });
|
||||
} else {
|
||||
return onChange({ [question.id]: [...value, item] });
|
||||
onChange({ [question.id]: [...newValue, item] });
|
||||
return;
|
||||
}
|
||||
onChange({ [question.id]: [...value, item] });
|
||||
return;
|
||||
}
|
||||
return onChange({ [question.id]: [item] }); // if not array, make it an array
|
||||
onChange({ [question.id]: [item] }); // if not array, make it an array
|
||||
};
|
||||
|
||||
const removeItem = (item: string) => {
|
||||
if (Array.isArray(value)) {
|
||||
return onChange({ [question.id]: value.filter((i) => i !== item) });
|
||||
onChange({ [question.id]: value.filter((i) => i !== item) });
|
||||
return;
|
||||
}
|
||||
return onChange({ [question.id]: [] }); // if not array, make it an array
|
||||
onChange({ [question.id]: [] }); // if not array, make it an array
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -135,7 +138,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const newValue = (value as string[])?.filter((item) => {
|
||||
const newValue = value.filter((item) => {
|
||||
return getChoicesWithoutOtherLabels().includes(item) || item === otherValue;
|
||||
}); // filter out all those values which are either in getChoicesWithoutOtherLabels() (i.e. selected by checkbox) or the latest entered otherValue
|
||||
onChange({ [question.id]: newValue });
|
||||
@@ -146,7 +149,9 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -191,7 +196,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
onChange={(e) => {
|
||||
if ((e.target as HTMLInputElement)?.checked) {
|
||||
if ((e.target as HTMLInputElement).checked) {
|
||||
addItem(getLocalizedValue(choice.label, languageCode));
|
||||
} else {
|
||||
removeItem(getLocalizedValue(choice.label, languageCode));
|
||||
@@ -214,7 +219,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{otherOption && (
|
||||
{otherOption ? (
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
@@ -254,7 +259,7 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
dir="auto"
|
||||
@@ -273,9 +278,9 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</label>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -302,4 +307,4 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, getShuffledChoicesIds } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
@@ -26,7 +26,7 @@ interface MultipleChoiceSingleProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const MultipleChoiceSingleQuestion = ({
|
||||
export function MultipleChoiceSingleQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -39,7 +39,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
setTtc,
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
}: MultipleChoiceSingleProps) => {
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [otherSelected, setOtherSelected] = useState(false);
|
||||
const otherSpecify = useRef<HTMLInputElement | null>(null);
|
||||
@@ -49,20 +49,21 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
const shuffledChoicesIds = useMemo(() => {
|
||||
if (question.shuffleOption) {
|
||||
return getShuffledChoicesIds(question.choices, question.shuffleOption);
|
||||
} else return question.choices.map((choice) => choice.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}
|
||||
return question.choices.map((choice) => choice.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- only want to run this effect when question.choices changes
|
||||
}, [question.shuffleOption, question.choices.length, question.choices[question.choices.length - 1].id]);
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const questionChoices = useMemo(() => {
|
||||
if (!question.choices) {
|
||||
if (!question.choices.length) {
|
||||
return [];
|
||||
}
|
||||
if (question.shuffleOption === "none" || question.shuffleOption === undefined) return question.choices;
|
||||
return shuffledChoicesIds.map((choiceId) => {
|
||||
const choice = question.choices.find((choice) => {
|
||||
return choice.id === choiceId;
|
||||
const choice = question.choices.find((selectedChoice) => {
|
||||
return selectedChoice.id === choiceId;
|
||||
});
|
||||
return choice;
|
||||
});
|
||||
@@ -109,7 +110,9 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -164,7 +167,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
onChange({ [question.id]: getLocalizedValue(choice.label, languageCode) });
|
||||
}}
|
||||
checked={value === getLocalizedValue(choice.label, languageCode)}
|
||||
required={question.required && idx === 0}
|
||||
required={question.required ? idx === 0 : undefined}
|
||||
/>
|
||||
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
|
||||
{getLocalizedValue(choice.label, languageCode)}
|
||||
@@ -173,7 +176,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{otherOption && (
|
||||
{otherOption ? (
|
||||
<label
|
||||
dir="auto"
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
@@ -214,7 +217,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
{getLocalizedValue(otherOption.label, languageCode)}
|
||||
</span>
|
||||
</span>
|
||||
{otherSelected && (
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
@@ -226,14 +229,16 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
}}
|
||||
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
|
||||
placeholder={
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
|
||||
getLocalizedValue(question.otherOptionPlaceholder, languageCode).length > 0
|
||||
? getLocalizedValue(question.otherOptionPlaceholder, languageCode)
|
||||
: "Please specify"
|
||||
}
|
||||
required={question.required}
|
||||
aria-labelledby={`${otherOption.id}-label`}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</label>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -259,4 +264,4 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyNPSQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface NPSQuestionProps {
|
||||
@@ -26,7 +26,7 @@ interface NPSQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const NPSQuestion = ({
|
||||
export function NPSQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -38,7 +38,7 @@ export const NPSQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: NPSQuestionProps) => {
|
||||
}: NPSQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -60,7 +60,9 @@ export const NPSQuestion = ({
|
||||
};
|
||||
|
||||
const getNPSOptionColor = (idx: number) => {
|
||||
return idx > 8 ? "fb-bg-emerald-100" : idx > 6 ? "fb-bg-orange-100" : "fb-bg-rose-100";
|
||||
if (idx > 8) return "fb-bg-emerald-100";
|
||||
if (idx > 6) return "fb-bg-orange-100";
|
||||
return "fb-bg-rose-100";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -74,7 +76,9 @@ export const NPSQuestion = ({
|
||||
}}>
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -93,8 +97,12 @@ export const NPSQuestion = ({
|
||||
<label
|
||||
key={number}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
onMouseOver={() => setHoveredNumber(number)}
|
||||
onMouseLeave={() => setHoveredNumber(-1)}
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredNumber(-1);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
if (e.key === " ") {
|
||||
@@ -113,11 +121,11 @@ export const NPSQuestion = ({
|
||||
: "fb-h fb-leading-10",
|
||||
hoveredNumber === number ? "fb-bg-accent-bg" : ""
|
||||
)}>
|
||||
{question.isColorCodingEnabled && (
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getNPSOptionColor(idx)}`}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<input
|
||||
type="radio"
|
||||
id={number.toString()}
|
||||
@@ -125,7 +133,9 @@ export const NPSQuestion = ({
|
||||
value={number}
|
||||
checked={value === number}
|
||||
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => handleClick(number)}
|
||||
onClick={() => {
|
||||
handleClick(number);
|
||||
}}
|
||||
required={question.required}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
@@ -164,4 +174,4 @@ export const NPSQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { RefObject } from "preact";
|
||||
import { type RefObject } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface OpenTextQuestionProps {
|
||||
@@ -28,7 +27,7 @@ interface OpenTextQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const OpenTextQuestion = ({
|
||||
export function OpenTextQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -41,7 +40,7 @@ export const OpenTextQuestion = ({
|
||||
setTtc,
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
}: OpenTextQuestionProps) => {
|
||||
}: OpenTextQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
@@ -60,7 +59,7 @@ export const OpenTextQuestion = ({
|
||||
};
|
||||
|
||||
const handleInputResize = (event: { target: any }) => {
|
||||
let maxHeight = 160; // 8 lines
|
||||
const maxHeight = 160; // 8 lines
|
||||
const textarea = event.target;
|
||||
textarea.style.height = "auto";
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
@@ -80,7 +79,9 @@ export const OpenTextQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -94,17 +95,19 @@ export const OpenTextQuestion = ({
|
||||
{question.longAnswer === false ? (
|
||||
<input
|
||||
ref={inputRef as RefObject<HTMLInputElement>}
|
||||
autoFocus={isCurrent && autoFocusEnabled}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
name={question.id}
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir="auto"
|
||||
step={"any"}
|
||||
step="any"
|
||||
required={question.required}
|
||||
value={value ? (value as string) : ""}
|
||||
value={value ? value : ""}
|
||||
type={question.inputType}
|
||||
onInput={(e) => handleInputChange(e.currentTarget.value)}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
}}
|
||||
className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm"
|
||||
pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"}
|
||||
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
|
||||
@@ -113,7 +116,7 @@ export const OpenTextQuestion = ({
|
||||
<textarea
|
||||
ref={inputRef as RefObject<HTMLTextAreaElement>}
|
||||
rows={3}
|
||||
autoFocus={isCurrent && autoFocusEnabled}
|
||||
autoFocus={isCurrent ? autoFocusEnabled : undefined}
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
aria-label="textarea"
|
||||
@@ -121,7 +124,7 @@ export const OpenTextQuestion = ({
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
dir="auto"
|
||||
required={question.required}
|
||||
value={value as string}
|
||||
value={value}
|
||||
type={question.inputType}
|
||||
onInput={(e) => {
|
||||
handleInputChange(e.currentTarget.value);
|
||||
@@ -156,4 +159,4 @@ export const OpenTextQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyPictureSelectionQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface PictureSelectionProps {
|
||||
@@ -26,7 +26,7 @@ interface PictureSelectionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const PictureSelectionQuestion = ({
|
||||
export function PictureSelectionQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -38,7 +38,7 @@ export const PictureSelectionQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: PictureSelectionProps) => {
|
||||
}: PictureSelectionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
@@ -53,7 +53,7 @@ export const PictureSelectionQuestion = ({
|
||||
values = [item];
|
||||
}
|
||||
|
||||
return onChange({ [question.id]: values });
|
||||
onChange({ [question.id]: values });
|
||||
};
|
||||
|
||||
const removeItem = (item: string) => {
|
||||
@@ -65,7 +65,7 @@ export const PictureSelectionQuestion = ({
|
||||
values = [];
|
||||
}
|
||||
|
||||
return onChange({ [question.id]: values });
|
||||
onChange({ [question.id]: values });
|
||||
};
|
||||
|
||||
const handleChange = (id: string) => {
|
||||
@@ -80,7 +80,7 @@ export const PictureSelectionQuestion = ({
|
||||
if (!question.allowMulti && value.length > 1) {
|
||||
onChange({ [question.id]: [] });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to recompute when the allowMulti changes
|
||||
}, [question.allowMulti]);
|
||||
|
||||
const questionChoices = question.choices;
|
||||
@@ -97,7 +97,9 @@ export const PictureSelectionQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -124,7 +126,9 @@ export const PictureSelectionQuestion = ({
|
||||
document.getElementById(choice.id)?.focus();
|
||||
}
|
||||
}}
|
||||
onClick={() => handleChange(choice.id)}
|
||||
onClick={() => {
|
||||
handleChange(choice.id);
|
||||
}}
|
||||
className={cn(
|
||||
"fb-relative fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border fb-rounded-custom focus:fb-outline-none fb-aspect-[4/3] fb-min-h-[7rem] fb-max-h-[50vh] focus:fb-border-brand focus:fb-border-4 group/image",
|
||||
Array.isArray(value) && value.includes(choice.id)
|
||||
@@ -143,7 +147,9 @@ export const PictureSelectionQuestion = ({
|
||||
target="_blank"
|
||||
title="Open in new tab"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="fb-absolute fb-bottom-2 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-slate-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-opacity-0 fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -153,9 +159,9 @@ export const PictureSelectionQuestion = ({
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-expand">
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-expand">
|
||||
<path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8" />
|
||||
<path d="M3 16.2V21m0 0h4.8M3 21l6-6" />
|
||||
<path d="M21 7.8V3m0 0h-4.8M21 3l-6 6" />
|
||||
@@ -216,4 +222,4 @@ export const PictureSelectionQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { Subheader } from "@/components/general/Subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { Subheader } from "@/components/general/subheader";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, getShuffledChoicesIds } from "@/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useCallback, useMemo, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type {
|
||||
TSurveyQuestionChoice,
|
||||
TSurveyQuestionId,
|
||||
@@ -31,7 +31,7 @@ interface RankingQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const RankingQuestion = ({
|
||||
export function RankingQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -44,13 +44,14 @@ export const RankingQuestion = ({
|
||||
setTtc,
|
||||
autoFocusEnabled,
|
||||
currentQuestionId,
|
||||
}: RankingQuestionProps) => {
|
||||
}: RankingQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
const shuffledChoicesIds = useMemo(() => {
|
||||
if (question.shuffleOption) {
|
||||
return getShuffledChoicesIds(question.choices, question.shuffleOption);
|
||||
} else return question.choices.map((choice) => choice.id);
|
||||
}
|
||||
return question.choices.map((choice) => choice.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [question.shuffleOption, question.choices.length]);
|
||||
|
||||
@@ -61,7 +62,7 @@ export const RankingQuestion = ({
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
|
||||
const [localValue, setLocalValue] = useState<string[]>(value || []);
|
||||
const [localValue, setLocalValue] = useState<string[]>(value ?? []);
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
return localValue
|
||||
@@ -72,9 +73,8 @@ export const RankingQuestion = ({
|
||||
const unsortedItems = useMemo(() => {
|
||||
if (question.shuffleOption === "all" && sortedItems.length === 0) {
|
||||
return shuffledChoicesIds.map((id) => question.choices.find((c) => c.id === id));
|
||||
} else {
|
||||
return question.choices.filter((c) => !localValue.includes(c.id));
|
||||
}
|
||||
return question.choices.filter((c) => !localValue.includes(c.id));
|
||||
}, [question.choices, question.shuffleOption, localValue, sortedItems, shuffledChoicesIds]);
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
@@ -144,7 +144,9 @@ export const RankingQuestion = ({
|
||||
<form onSubmit={handleSubmit} className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -180,7 +182,9 @@ export const RankingQuestion = ({
|
||||
autoFocus={idx === 0 && autoFocusEnabled}>
|
||||
<div
|
||||
className="fb-flex fb-gap-x-4 fb-px-4 fb-items-center fb-grow fb-h-full group"
|
||||
onClick={() => handleItemClick(item)}>
|
||||
onClick={() => {
|
||||
handleItemClick(item);
|
||||
}}>
|
||||
<span
|
||||
className={cn(
|
||||
"fb-w-6 fb-grow-0 fb-h-6 fb-flex fb-items-center fb-justify-center fb-rounded-full fb-text-xs fb-font-semibold fb-border-brand fb-border",
|
||||
@@ -194,12 +198,14 @@ export const RankingQuestion = ({
|
||||
{getLocalizedValue(item.label, languageCode)}
|
||||
</div>
|
||||
</div>
|
||||
{isSorted && (
|
||||
{isSorted ? (
|
||||
<div className="fb-flex fb-flex-col fb-h-full fb-grow-0 fb-border-l fb-border-border">
|
||||
<button
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
onClick={() => handleMove(item.id, "up")}
|
||||
onClick={() => {
|
||||
handleMove(item.id, "up");
|
||||
}}
|
||||
className={cn(
|
||||
"fb-px-2 fb-flex fb-flex-1 fb-items-center fb-justify-center",
|
||||
isFirst
|
||||
@@ -224,7 +230,9 @@ export const RankingQuestion = ({
|
||||
<button
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
onClick={() => handleMove(item.id, "down")}
|
||||
onClick={() => {
|
||||
handleMove(item.id, "down");
|
||||
}}
|
||||
className={cn(
|
||||
"fb-px-2 fb-flex-1 fb-border-t fb-border-border fb-flex fb-items-center fb-justify-center",
|
||||
isLast
|
||||
@@ -247,14 +255,14 @@ export const RankingQuestion = ({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{error && <div className="fb-text-red-500 fb-mt-2 fb-text-sm">{error}</div>}
|
||||
{error ? <div className="fb-text-red-500 fb-mt-2 fb-text-sm">{error}</div> : null}
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
@@ -274,4 +282,4 @@ export const RankingQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import { SubmitButton } from "@/components/buttons/SubmitButton";
|
||||
import { Headline } from "@/components/general/Headline";
|
||||
import { QuestionMedia } from "@/components/general/QuestionMedia";
|
||||
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
|
||||
import { BackButton } from "@/components/buttons/back-button";
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { Headline } from "@/components/general/headline";
|
||||
import { QuestionMedia } from "@/components/general/question-media";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyQuestionId, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
ConfusedFace,
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
SmilingFaceWithSmilingEyes,
|
||||
TiredFace,
|
||||
WearyFace,
|
||||
} from "../general/Smileys";
|
||||
import { Subheader } from "../general/Subheader";
|
||||
} from "../general/smileys";
|
||||
import { Subheader } from "../general/subheader";
|
||||
|
||||
interface RatingQuestionProps {
|
||||
question: TSurveyRatingQuestion;
|
||||
@@ -39,7 +39,7 @@ interface RatingQuestionProps {
|
||||
currentQuestionId: TSurveyQuestionId;
|
||||
}
|
||||
|
||||
export const RatingQuestion = ({
|
||||
export function RatingQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
@@ -51,7 +51,7 @@ export const RatingQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: RatingQuestionProps) => {
|
||||
}: RatingQuestionProps) {
|
||||
const [hoveredNumber, setHoveredNumber] = useState(0);
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
@@ -72,18 +72,22 @@ export const RatingQuestion = ({
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const HiddenRadioInput = ({ number, id }: { number: number; id?: string }) => (
|
||||
<input
|
||||
type="radio"
|
||||
id={id}
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-invisible fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => handleSelect(number)}
|
||||
required={question.required}
|
||||
checked={value === number}
|
||||
/>
|
||||
);
|
||||
function HiddenRadioInput({ number, id }: { number: number; id?: string }) {
|
||||
return (
|
||||
<input
|
||||
type="radio"
|
||||
id={id}
|
||||
name="rating"
|
||||
value={number}
|
||||
className="fb-invisible fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
|
||||
onClick={() => {
|
||||
handleSelect(number);
|
||||
}}
|
||||
required={question.required}
|
||||
checked={value === number}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setHoveredNumber(0);
|
||||
@@ -98,11 +102,10 @@ export const RatingQuestion = ({
|
||||
if (range - idx < 1) return "fb-bg-emerald-100";
|
||||
if (range - idx < 2) return "fb-bg-orange-100";
|
||||
return "fb-bg-rose-100";
|
||||
} else {
|
||||
if (range - idx < 2) return "fb-bg-emerald-100";
|
||||
if (range - idx < 3) return "fb-bg-orange-100";
|
||||
return "fb-bg-rose-100";
|
||||
}
|
||||
if (range - idx < 2) return "fb-bg-emerald-100";
|
||||
if (range - idx < 3) return "fb-bg-orange-100";
|
||||
return "fb-bg-rose-100";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -117,7 +120,9 @@ export const RatingQuestion = ({
|
||||
className="fb-w-full">
|
||||
<ScrollableContainer>
|
||||
<div>
|
||||
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
|
||||
{isMediaAvailable ? (
|
||||
<QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />
|
||||
) : null}
|
||||
<Headline
|
||||
headline={getLocalizedValue(question.headline, languageCode)}
|
||||
questionId={question.id}
|
||||
@@ -134,8 +139,12 @@ export const RatingQuestion = ({
|
||||
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
|
||||
<span
|
||||
key={number}
|
||||
onMouseOver={() => setHoveredNumber(number)}
|
||||
onMouseLeave={() => setHoveredNumber(0)}
|
||||
onMouseOver={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredNumber(0);
|
||||
}}
|
||||
className="fb-bg-survey-bg fb-flex-1 fb-text-center fb-text-sm">
|
||||
{question.scale === "number" ? (
|
||||
<label
|
||||
@@ -158,11 +167,11 @@ export const RatingQuestion = ({
|
||||
question.isColorCodingEnabled ? "fb-min-h-[47px]" : "fb-min-h-[41px]",
|
||||
"fb-text-heading focus:fb-border-brand fb-relative fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-justify-center fb-overflow-hidden fb-border-b fb-border-l fb-border-t focus:fb-border-2 focus:fb-outline-none"
|
||||
)}>
|
||||
{question.isColorCodingEnabled && (
|
||||
{question.isColorCodingEnabled ? (
|
||||
<div
|
||||
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getRatingNumberOptionColor(question.range, number)}`}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
{number}
|
||||
</label>
|
||||
@@ -178,14 +187,18 @@ export const RatingQuestion = ({
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
number <= hoveredNumber || number <= (value as number)
|
||||
number <= hoveredNumber || number <= value!
|
||||
? "fb-text-amber-400"
|
||||
: "fb-text-[#8696AC]",
|
||||
hoveredNumber === number ? "fb-text-amber-400" : "",
|
||||
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-cursor-pointer fb-justify-center focus:fb-outline-none"
|
||||
)}
|
||||
onFocus={() => setHoveredNumber(number)}
|
||||
onBlur={() => setHoveredNumber(0)}>
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(0);
|
||||
}}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className="fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
@@ -213,8 +226,12 @@ export const RatingQuestion = ({
|
||||
document.getElementById(number.toString())?.focus();
|
||||
}
|
||||
}}
|
||||
onFocus={() => setHoveredNumber(number)}
|
||||
onBlur={() => setHoveredNumber(0)}>
|
||||
onFocus={() => {
|
||||
setHoveredNumber(number);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHoveredNumber(0);
|
||||
}}>
|
||||
<HiddenRadioInput number={number} id={number.toString()} />
|
||||
<div className={cn("fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain")}>
|
||||
<RatingSmiley
|
||||
@@ -249,7 +266,7 @@ export const RatingQuestion = ({
|
||||
isLastQuestion={isLastQuestion}
|
||||
/>
|
||||
)}
|
||||
<div></div>
|
||||
<div />
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
@@ -264,7 +281,7 @@ export const RatingQuestion = ({
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
interface RatingSmileyProps {
|
||||
active: boolean;
|
||||
@@ -282,11 +299,10 @@ const getSmileyColor = (range: number, idx: number) => {
|
||||
if (range - idx < 2) return "fb-fill-emerald-100";
|
||||
if (range - idx < 3) return "fb-fill-orange-100";
|
||||
return "fb-fill-rose-100";
|
||||
} else {
|
||||
if (range - idx < 3) return "fb-fill-emerald-100";
|
||||
if (range - idx < 4) return "fb-fill-orange-100";
|
||||
return "fb-fill-rose-100";
|
||||
}
|
||||
if (range - idx < 3) return "fb-fill-emerald-100";
|
||||
if (range - idx < 4) return "fb-fill-orange-100";
|
||||
return "fb-fill-rose-100";
|
||||
};
|
||||
|
||||
const getActiveSmileyColor = (range: number, idx: number) => {
|
||||
@@ -298,11 +314,10 @@ const getActiveSmileyColor = (range: number, idx: number) => {
|
||||
if (range - idx < 2) return "fb-fill-emerald-300";
|
||||
if (range - idx < 3) return "fb-fill-orange-300";
|
||||
return "fb-fill-rose-300";
|
||||
} else {
|
||||
if (range - idx < 3) return "fb-fill-emerald-300";
|
||||
if (range - idx < 4) return "fb-fill-orange-300";
|
||||
return "fb-fill-rose-300";
|
||||
}
|
||||
if (range - idx < 3) return "fb-fill-emerald-300";
|
||||
if (range - idx < 4) return "fb-fill-orange-300";
|
||||
return "fb-fill-rose-300";
|
||||
};
|
||||
|
||||
const getSmiley = (iconIdx: number, idx: number, range: number, active: boolean, addColors: boolean) => {
|
||||
@@ -310,18 +325,23 @@ const getSmiley = (iconIdx: number, idx: number, range: number, active: boolean,
|
||||
const inactiveColor = addColors ? getSmileyColor(range, idx) : "fb-fill-none";
|
||||
|
||||
const icons = [
|
||||
<TiredFace className={active ? activeColor : inactiveColor} />,
|
||||
<WearyFace className={active ? activeColor : inactiveColor} />,
|
||||
<PerseveringFace className={active ? activeColor : inactiveColor} />,
|
||||
<FrowningFace className={active ? activeColor : inactiveColor} />,
|
||||
<ConfusedFace className={active ? activeColor : inactiveColor} />,
|
||||
<NeutralFace className={active ? activeColor : inactiveColor} />,
|
||||
<SlightlySmilingFace className={active ? activeColor : inactiveColor} />,
|
||||
<SmilingFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
|
||||
<GrinningFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
|
||||
<GrinningSquintingFace className={active ? activeColor : inactiveColor} />,
|
||||
<TiredFace key="tired-face" className={active ? activeColor : inactiveColor} />,
|
||||
<WearyFace key="weary-face" className={active ? activeColor : inactiveColor} />,
|
||||
<PerseveringFace key="persevering-face" className={active ? activeColor : inactiveColor} />,
|
||||
<FrowningFace key="frowning-face" className={active ? activeColor : inactiveColor} />,
|
||||
<ConfusedFace key="confused-face" className={active ? activeColor : inactiveColor} />,
|
||||
<NeutralFace key="neutral-face" className={active ? activeColor : inactiveColor} />,
|
||||
<SlightlySmilingFace key="slightly-smiling-face" className={active ? activeColor : inactiveColor} />,
|
||||
<SmilingFaceWithSmilingEyes
|
||||
key="smiling-face-with-smiling-eyes"
|
||||
className={active ? activeColor : inactiveColor}
|
||||
/>,
|
||||
<GrinningFaceWithSmilingEyes
|
||||
key="grinning-face-with-smiling-eyes"
|
||||
className={active ? activeColor : inactiveColor}
|
||||
/>,
|
||||
<GrinningSquintingFace key="grinning-squinting-face" className={active ? activeColor : inactiveColor} />,
|
||||
];
|
||||
|
||||
return icons[iconIdx];
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { AutoCloseProgressBar } from "@/components/general/AutoCloseProgressBar";
|
||||
import { AutoCloseProgressBar } from "@/components/general/auto-close-progress-bar";
|
||||
import React from "preact/compat";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
|
||||
interface AutoCloseProps {
|
||||
survey: TJsEnvironmentStateSurvey;
|
||||
onClose: () => void;
|
||||
onClose?: () => void;
|
||||
offset: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AutoCloseWrapper = ({ survey, onClose, children, offset }: AutoCloseProps) => {
|
||||
export function AutoCloseWrapper({ survey, onClose, children, offset }: AutoCloseProps) {
|
||||
const [countDownActive, setCountDownActive] = useState(true);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const isAppSurvey = survey.type === "app";
|
||||
@@ -24,7 +24,7 @@ export const AutoCloseWrapper = ({ survey, onClose, children, offset }: AutoClos
|
||||
}
|
||||
setCountDownActive(true);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
onClose();
|
||||
onClose?.();
|
||||
setCountDownActive(false);
|
||||
}, survey.autoClose * 1000);
|
||||
};
|
||||
@@ -40,17 +40,17 @@ export const AutoCloseWrapper = ({ survey, onClose, children, offset }: AutoClos
|
||||
useEffect(() => {
|
||||
startCountdown();
|
||||
return stopCountdown;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to run this effect on every render
|
||||
}, [survey.autoClose]);
|
||||
|
||||
return (
|
||||
<div className="fb-h-full fb-w-full">
|
||||
{survey.autoClose && showAutoCloseProgressBar && (
|
||||
{survey.autoClose && showAutoCloseProgressBar ? (
|
||||
<AutoCloseProgressBar autoCloseTimeout={survey.autoClose} />
|
||||
)}
|
||||
) : null}
|
||||
<div onClick={stopCountdown} onMouseOver={stopCountdown} className="fb-h-full fb-w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { VNode } from "preact";
|
||||
import { type VNode } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
import { type TPlacement } from "@formbricks/types/common";
|
||||
|
||||
interface ModalProps {
|
||||
children: VNode;
|
||||
@@ -13,7 +13,7 @@ interface ModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const Modal = ({ children, isOpen, placement, clickOutside, darkOverlay, onClose }: ModalProps) => {
|
||||
export function Modal({ children, isOpen, placement, clickOutside, darkOverlay, onClose }: ModalProps) {
|
||||
const [show, setShow] = useState(false);
|
||||
const isCenter = placement === "center";
|
||||
const modalRef = useRef(null);
|
||||
@@ -42,8 +42,8 @@ export const Modal = ({ children, isOpen, placement, clickOutside, darkOverlay,
|
||||
}, [show, clickOutside, onClose, isCenter]);
|
||||
|
||||
// This classes will be applied only when screen size is greater than sm, hence sm is common prefix for all
|
||||
const getPlacementStyle = (placement: TPlacement) => {
|
||||
switch (placement) {
|
||||
const getPlacementStyle = (contentPlacement: TPlacement) => {
|
||||
switch (contentPlacement) {
|
||||
case "bottomRight":
|
||||
return "sm:fb-bottom-3 sm:fb-right-3";
|
||||
case "topRight":
|
||||
@@ -71,11 +71,9 @@ export const Modal = ({ children, isOpen, placement, clickOutside, darkOverlay,
|
||||
<div
|
||||
className={cn(
|
||||
"fb-relative fb-h-full fb-w-full",
|
||||
isCenter
|
||||
? darkOverlay
|
||||
? "fb-bg-slate-700/80"
|
||||
: "fb-bg-white/50"
|
||||
: "fb-bg-none fb-transition-all fb-duration-500 fb-ease-in-out"
|
||||
!isCenter ? "fb-bg-none fb-transition-all fb-duration-500 fb-ease-in-out" : "",
|
||||
isCenter && darkOverlay ? "fb-bg-slate-700/80" : "",
|
||||
isCenter && !darkOverlay ? "fb-bg-white/50" : ""
|
||||
)}>
|
||||
<div
|
||||
ref={modalRef}
|
||||
@@ -89,4 +87,4 @@ export const Modal = ({ children, isOpen, placement, clickOutside, darkOverlay,
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ScrollableContainerProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
|
||||
export function ScrollableContainer({ children }: ScrollableContainerProps) {
|
||||
const [isOverflowHidden, setIsOverflowHidden] = useState(true);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
const [isAtTop, setIsAtTop] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isSurveyPreview = !!document.getElementById("survey-preview");
|
||||
const isSurveyPreview = Boolean(document.getElementById("survey-preview"));
|
||||
|
||||
const checkScroll = () => {
|
||||
if (!containerRef.current) return;
|
||||
@@ -26,7 +26,7 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
|
||||
const toggleOverflow = (hide: boolean) => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
if (hide) {
|
||||
timeoutRef.current = setTimeout(() => setIsOverflowHidden(true), 1000);
|
||||
timeoutRef.current = setTimeout(() => { setIsOverflowHidden(true); }, 1000);
|
||||
} else {
|
||||
setIsOverflowHidden(false);
|
||||
checkScroll();
|
||||
@@ -37,7 +37,7 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
|
||||
const element = containerRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const handleScroll = () => checkScroll();
|
||||
const handleScroll = () => { checkScroll(); };
|
||||
element.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
@@ -52,7 +52,7 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
|
||||
return (
|
||||
<div className="fb-relative">
|
||||
{!isAtTop && (
|
||||
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-4 fb-bg-gradient-to-b fb-to-transparent"></div>
|
||||
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-4 fb-bg-gradient-to-b fb-to-transparent" />
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -64,13 +64,13 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
|
||||
"fb-overflow-auto fb-px-4 fb-pb-1",
|
||||
isOverflowHidden ? "fb-no-scrollbar" : "fb-bg-survey-bg"
|
||||
)}
|
||||
onMouseEnter={() => toggleOverflow(false)}
|
||||
onMouseLeave={() => toggleOverflow(true)}>
|
||||
onMouseEnter={() => { toggleOverflow(false); }}
|
||||
onMouseLeave={() => { toggleOverflow(true); }}>
|
||||
{children}
|
||||
</div>
|
||||
{!isAtBottom && (
|
||||
<div className="fb-from-survey-bg fb-absolute -fb-bottom-2 fb-left-0 fb-right-2 fb-h-8 fb-bg-gradient-to-t fb-to-transparent"></div>
|
||||
<div className="fb-from-survey-bg fb-absolute -fb-bottom-2 fb-left-0 fb-right-2 fb-h-8 fb-bg-gradient-to-t fb-to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { TSurveyQuestionId, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TProjectStyling } from "@formbricks/types/project";
|
||||
import { type TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { type TSurveyQuestionId, type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
|
||||
// offset = 0 -> Current question card
|
||||
// offset < 0 -> Question cards that are already answered
|
||||
@@ -20,7 +20,7 @@ interface StackedCardsContainerProps {
|
||||
fullSizeCards: boolean;
|
||||
}
|
||||
|
||||
export const StackedCardsContainer = ({
|
||||
export function StackedCardsContainer({
|
||||
cardArrangement,
|
||||
currentQuestionId,
|
||||
survey,
|
||||
@@ -29,11 +29,11 @@ export const StackedCardsContainer = ({
|
||||
setQuestionId,
|
||||
shouldResetQuestionId = true,
|
||||
fullSizeCards = false,
|
||||
}: StackedCardsContainerProps) => {
|
||||
}: StackedCardsContainerProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const highlightBorderColor =
|
||||
survey.styling?.highlightBorderColor?.light || styling.highlightBorderColor?.light;
|
||||
const cardBorderColor = survey.styling?.cardBorderColor?.light || styling.cardBorderColor?.light;
|
||||
survey.styling?.highlightBorderColor?.light ?? styling.highlightBorderColor?.light;
|
||||
const cardBorderColor = survey.styling?.cardBorderColor?.light ?? styling.cardBorderColor?.light;
|
||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const resizeObserver = useRef<ResizeObserver | null>(null);
|
||||
const [cardHeight, setCardHeight] = useState("auto");
|
||||
@@ -45,7 +45,7 @@ export const StackedCardsContainer = ({
|
||||
return survey.questions.length;
|
||||
}
|
||||
return survey.questions.findIndex((question) => question.id === currentQuestionId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only update when currentQuestionId changes
|
||||
}, [currentQuestionId, survey.welcomeCard.enabled, survey.questions.length]);
|
||||
|
||||
const [prevQuestionIdx, setPrevQuestionIdx] = useState(questionIdxTemp - 1);
|
||||
@@ -75,7 +75,7 @@ export const StackedCardsContainer = ({
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only update when questionIdxTemp changes
|
||||
}, [questionIdxTemp]);
|
||||
|
||||
const borderStyles = useMemo(() => {
|
||||
@@ -88,20 +88,29 @@ export const StackedCardsContainer = ({
|
||||
survey.type === "link" || !highlightBorderColor ? cardBorderColor : highlightBorderColor;
|
||||
return {
|
||||
...baseStyle,
|
||||
borderColor: borderColor,
|
||||
borderColor,
|
||||
};
|
||||
}, [survey.type, cardBorderColor, highlightBorderColor]);
|
||||
|
||||
const calculateCardTransform = useMemo(() => {
|
||||
const rotationCoefficient = cardWidth >= 1000 ? 1.5 : cardWidth > 650 ? 2 : 3;
|
||||
let rotationCoefficient = 3;
|
||||
|
||||
if (cardWidth >= 1000) {
|
||||
rotationCoefficient = 1.5;
|
||||
} else if (cardWidth > 650) {
|
||||
rotationCoefficient = 2;
|
||||
}
|
||||
|
||||
return (offset: number) => {
|
||||
switch (cardArrangement) {
|
||||
case "casual":
|
||||
return offset < 0
|
||||
? `translateX(33%)`
|
||||
: `translateX(0) rotate(-${(hovered ? rotationCoefficient : rotationCoefficient - 0.5) * offset}deg)`;
|
||||
: `translateX(0) rotate(-${((hovered ? rotationCoefficient : rotationCoefficient - 0.5) * offset).toString()}deg)`;
|
||||
case "straight":
|
||||
return offset < 0 ? `translateY(25%)` : `translateY(-${(hovered ? 12 : 10) * offset}px)`;
|
||||
return offset < 0
|
||||
? `translateY(25%)`
|
||||
: `translateY(-${((hovered ? 12 : 10) * offset).toString()}px)`;
|
||||
default:
|
||||
return offset < 0 ? `translateX(0)` : `translateX(0)`;
|
||||
}
|
||||
@@ -112,7 +121,7 @@ export const StackedCardsContainer = ({
|
||||
if (cardArrangement === "straight") {
|
||||
// styles to set the descending width of stacked question cards when card arrangement is set to straight
|
||||
return {
|
||||
width: `${100 - 5 * offset >= 100 ? 100 : 100 - 5 * offset}%`,
|
||||
width: `${(100 - 5 * offset >= 100 ? 100 : 100 - 5 * offset).toString()}%`,
|
||||
margin: "auto",
|
||||
};
|
||||
}
|
||||
@@ -128,7 +137,7 @@ export const StackedCardsContainer = ({
|
||||
}
|
||||
resizeObserver.current = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setCardHeight(entry.contentRect.height + "px");
|
||||
setCardHeight(`${entry.contentRect.height.toString()}px`);
|
||||
setCardWidth(entry.contentRect.width);
|
||||
}
|
||||
});
|
||||
@@ -144,9 +153,9 @@ export const StackedCardsContainer = ({
|
||||
// Reset question progress, when card arrangement changes
|
||||
useEffect(() => {
|
||||
if (shouldResetQuestionId) {
|
||||
setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
setQuestionId(survey.welcomeCard.enabled ? "start" : survey.questions[0]?.id);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only update when cardArrangement changes
|
||||
}, [cardArrangement]);
|
||||
|
||||
const getCardHeight = (offset: number): string => {
|
||||
@@ -155,7 +164,7 @@ export const StackedCardsContainer = ({
|
||||
// Preserve original height
|
||||
else if (offset < 0) return "initial";
|
||||
// Assign the height of the foremost card to all cards behind it
|
||||
else return cardHeight;
|
||||
return cardHeight;
|
||||
};
|
||||
|
||||
const getBottomStyles = () => {
|
||||
@@ -171,11 +180,13 @@ export const StackedCardsContainer = ({
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}>
|
||||
<div style={{ height: cardHeight }}></div>
|
||||
onMouseLeave={() => {
|
||||
setHovered(false);
|
||||
}}>
|
||||
<div style={{ height: cardHeight }} />
|
||||
{cardArrangement === "simple" ? (
|
||||
<div
|
||||
id={`questionCard-${questionIdxTemp}`}
|
||||
id={`questionCard-${questionIdxTemp.toString()}`}
|
||||
className={cn("fb-w-full fb-bg-survey-bg", fullSizeCards ? "fb-h-full" : "")}
|
||||
style={{
|
||||
...borderStyles,
|
||||
@@ -185,20 +196,20 @@ export const StackedCardsContainer = ({
|
||||
) : (
|
||||
questionIdxTemp !== undefined &&
|
||||
[prevQuestionIdx, currentQuestionIdx, nextQuestionIdx, nextQuestionIdx + 1].map(
|
||||
(questionIdxTemp, index) => {
|
||||
(dynamicQuestionIndex, index) => {
|
||||
const hasEndingCard = survey.endings.length > 0;
|
||||
// Check for hiding extra card
|
||||
if (questionIdxTemp > survey.questions.length + (hasEndingCard ? 0 : -1)) return;
|
||||
if (dynamicQuestionIndex > survey.questions.length + (hasEndingCard ? 0 : -1)) return;
|
||||
const offset = index - 1;
|
||||
const isHidden = offset < 0;
|
||||
return (
|
||||
<div
|
||||
ref={(el) => (cardRefs.current[questionIdxTemp] = el)}
|
||||
id={`questionCard-${questionIdxTemp}`}
|
||||
key={questionIdxTemp}
|
||||
ref={(el) => (cardRefs.current[dynamicQuestionIndex] = el)}
|
||||
id={`questionCard-${dynamicQuestionIndex}`}
|
||||
key={dynamicQuestionIndex}
|
||||
style={{
|
||||
zIndex: 1000 - questionIdxTemp,
|
||||
transform: `${calculateCardTransform(offset)}`,
|
||||
zIndex: 1000 - dynamicQuestionIndex,
|
||||
transform: calculateCardTransform(offset),
|
||||
opacity: isHidden ? 0 : (100 - 0 * offset) / 100,
|
||||
height: fullSizeCards ? "100%" : getCardHeight(offset),
|
||||
transitionDuration: "600ms",
|
||||
@@ -208,7 +219,7 @@ export const StackedCardsContainer = ({
|
||||
...getBottomStyles(),
|
||||
}}
|
||||
className="fb-pointer fb-rounded-custom fb-bg-survey-bg fb-absolute fb-inset-x-0 fb-backdrop-blur-md fb-transition-all fb-ease-in-out">
|
||||
{getCardContent(questionIdxTemp, offset)}
|
||||
{getCardContent(dynamicQuestionIndex, offset)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -216,4 +227,4 @@ export const StackedCardsContainer = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SurveyInline } from "@/components/general/SurveyInline";
|
||||
import { SurveyModal } from "@/components/general/SurveyModal";
|
||||
import { addCustomThemeToDom, addStylesToDom } from "@/lib/styles";
|
||||
import { h, render } from "preact";
|
||||
import { SurveyInlineProps, SurveyModalProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { type SurveyInlineProps, type SurveyModalProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { SurveyInline } from "@/components/general/survey-inline";
|
||||
import { SurveyModal } from "@/components/general/survey-modal";
|
||||
import { addCustomThemeToDom, addStylesToDom } from "@/lib/styles";
|
||||
|
||||
export const renderSurveyInline = (props: SurveyInlineProps) => {
|
||||
addStylesToDom();
|
||||
|
||||
@@ -2,8 +2,8 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { formatDateWithOrdinal, isValidDateString } from "@formbricks/lib/utils/datetime";
|
||||
import { extractFallbackValue, extractId, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||
import { type TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const replaceRecallInfo = (
|
||||
text: string,
|
||||
@@ -55,7 +55,7 @@ export const parseRecallInformation = (
|
||||
variables: TResponseVariables
|
||||
) => {
|
||||
const modifiedQuestion = structuredClone(question);
|
||||
if (question.headline && question.headline[languageCode]?.includes("recall:")) {
|
||||
if (question.headline[languageCode].includes("recall:")) {
|
||||
modifiedQuestion.headline[languageCode] = replaceRecallInfo(
|
||||
getLocalizedValue(modifiedQuestion.headline, languageCode),
|
||||
responseData,
|
||||
@@ -64,7 +64,7 @@ export const parseRecallInformation = (
|
||||
}
|
||||
if (
|
||||
question.subheader &&
|
||||
question.subheader[languageCode]?.includes("recall:") &&
|
||||
question.subheader[languageCode].includes("recall:") &&
|
||||
modifiedQuestion.subheader
|
||||
) {
|
||||
modifiedQuestion.subheader[languageCode] = replaceRecallInfo(
|
||||
|
||||
@@ -3,8 +3,8 @@ import preflight from "@/styles/preflight.css?inline";
|
||||
import calendarCss from "react-calendar/dist/Calendar.css?inline";
|
||||
import datePickerCss from "react-date-picker/dist/DatePicker.css?inline";
|
||||
import { isLight, mixColor } from "@formbricks/lib/utils/colors";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { type TProjectStyling } from "@formbricks/types/project";
|
||||
import { type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import editorCss from "../../../../apps/web/modules/ui/components/editor/styles-editor-frontend.css?inline";
|
||||
import datePickerCustomCss from "../styles/date-picker.css?inline";
|
||||
|
||||
@@ -18,7 +18,7 @@ export const addStylesToDom = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TSurveyStyling }) => {
|
||||
export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TSurveyStyling }): void => {
|
||||
// Check if the style element already exists
|
||||
let styleElement = document.getElementById("formbricks__css__custom");
|
||||
|
||||
@@ -45,9 +45,9 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
|
||||
// Use the helper function to append CSS variables
|
||||
appendCssVariable("brand-color", styling.brandColor?.light);
|
||||
appendCssVariable("focus-color", styling.brandColor?.light);
|
||||
if (!!styling.brandColor?.light) {
|
||||
if (styling.brandColor?.light) {
|
||||
// If the brand color is defined, set the text color based on the lightness of the brand color
|
||||
appendCssVariable("brand-text-color", isLight(styling.brandColor?.light) ? "black" : "white");
|
||||
appendCssVariable("brand-text-color", isLight(styling.brandColor.light) ? "black" : "white");
|
||||
} else {
|
||||
// If the brand color is undefined, default to white
|
||||
appendCssVariable("brand-text-color", "#ffffff");
|
||||
@@ -62,37 +62,37 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
|
||||
appendCssVariable("subheading-color", styling.questionColor?.light);
|
||||
|
||||
if (styling.questionColor?.light) {
|
||||
appendCssVariable("placeholder-color", mixColor(styling.questionColor?.light, "#ffffff", 0.3));
|
||||
appendCssVariable("placeholder-color", mixColor(styling.questionColor.light, "#ffffff", 0.3));
|
||||
}
|
||||
|
||||
appendCssVariable("border-color", styling.inputBorderColor?.light);
|
||||
|
||||
if (styling.inputBorderColor?.light) {
|
||||
appendCssVariable("border-color-highlight", mixColor(styling.inputBorderColor?.light, "#000000", 0.1));
|
||||
appendCssVariable("border-color-highlight", mixColor(styling.inputBorderColor.light, "#000000", 0.1));
|
||||
}
|
||||
|
||||
appendCssVariable("survey-background-color", styling.cardBackgroundColor?.light);
|
||||
appendCssVariable("survey-border-color", styling.cardBorderColor?.light);
|
||||
appendCssVariable("border-radius", `${roundness}px`);
|
||||
appendCssVariable("border-radius", `${Number(roundness).toString()}px`);
|
||||
appendCssVariable("input-background-color", styling.inputColor?.light);
|
||||
|
||||
if (styling.questionColor?.light) {
|
||||
let signatureColor = "";
|
||||
let brandingColor = "";
|
||||
|
||||
if (isLight(styling.questionColor?.light)) {
|
||||
signatureColor = mixColor(styling.questionColor?.light, "#000000", 0.2);
|
||||
brandingColor = mixColor(styling.questionColor?.light, "#000000", 0.3);
|
||||
if (isLight(styling.questionColor.light)) {
|
||||
signatureColor = mixColor(styling.questionColor.light, "#000000", 0.2);
|
||||
brandingColor = mixColor(styling.questionColor.light, "#000000", 0.3);
|
||||
} else {
|
||||
signatureColor = mixColor(styling.questionColor?.light, "#ffffff", 0.2);
|
||||
brandingColor = mixColor(styling.questionColor?.light, "#ffffff", 0.3);
|
||||
signatureColor = mixColor(styling.questionColor.light, "#ffffff", 0.2);
|
||||
brandingColor = mixColor(styling.questionColor.light, "#ffffff", 0.3);
|
||||
}
|
||||
|
||||
appendCssVariable("signature-text-color", signatureColor);
|
||||
appendCssVariable("branding-text-color", brandingColor);
|
||||
}
|
||||
|
||||
if (!!styling.inputColor?.light) {
|
||||
if (styling.inputColor?.light) {
|
||||
if (
|
||||
styling.inputColor.light === "#fff" ||
|
||||
styling.inputColor.light === "#ffffff" ||
|
||||
@@ -102,7 +102,7 @@ export const addCustomThemeToDom = ({ styling }: { styling: TProjectStyling | TS
|
||||
} else {
|
||||
appendCssVariable(
|
||||
"input-background-color-selected",
|
||||
mixColor(styling.inputColor?.light, "#000000", 0.025)
|
||||
mixColor(styling.inputColor.light, "#000000", 0.025)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const getUpdatedTtc = (ttc: TResponseTtc, questionId: TSurveyQuestionId, time: number) => {
|
||||
// Check if the question ID already exists
|
||||
if (ttc.hasOwnProperty(questionId)) {
|
||||
if (questionId in ttc) {
|
||||
return {
|
||||
...ttc,
|
||||
[questionId]: ttc[questionId] + time,
|
||||
};
|
||||
} else {
|
||||
// If the question ID does not exist, add it to the object
|
||||
return {
|
||||
...ttc,
|
||||
[questionId]: time,
|
||||
};
|
||||
}
|
||||
// If the question ID does not exist, add it to the object
|
||||
return {
|
||||
...ttc,
|
||||
[questionId]: time,
|
||||
};
|
||||
};
|
||||
|
||||
export const useTtc = (
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import {
|
||||
TShuffleOption,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
type TShuffleOption,
|
||||
type TSurveyLogic,
|
||||
type TSurveyLogicAction,
|
||||
type TSurveyQuestion,
|
||||
type TSurveyQuestionChoice,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
export const cn = (...classes: string[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
||||
|
||||
const shuffle = (array: any[]) => {
|
||||
const shuffle = (array: unknown[]) => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
@@ -20,7 +20,7 @@ const shuffle = (array: any[]) => {
|
||||
|
||||
export const getShuffledRowIndices = (n: number, shuffleOption: TShuffleOption): number[] => {
|
||||
// Create an array with numbers from 0 to n-1
|
||||
let array = Array.from(Array(n).keys());
|
||||
const array = Array.from(Array(n).keys());
|
||||
|
||||
if (shuffleOption === "all") {
|
||||
shuffle(array);
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -1008,6 +1008,9 @@ importers:
|
||||
'@formkit/auto-animate':
|
||||
specifier: 0.8.2
|
||||
version: 0.8.2
|
||||
react-calendar:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0(@types/react@18.3.11)(react-dom@19.0.0-rc-ed15d500-20241110(react@19.0.0-rc-ed15d500-20241110))(react@19.0.0-rc-ed15d500-20241110)
|
||||
devDependencies:
|
||||
'@calcom/embed-snippet':
|
||||
specifier: 1.3.0
|
||||
@@ -1027,6 +1030,9 @@ importers:
|
||||
'@preact/preset-vite':
|
||||
specifier: 2.9.0
|
||||
version: 2.9.0(@babel/core@7.25.2)(preact@10.23.2)(vite@5.4.8(@types/node@22.3.0)(terser@5.31.6))
|
||||
'@types/react':
|
||||
specifier: 18.3.11
|
||||
version: 18.3.11
|
||||
autoprefixer:
|
||||
specifier: 10.4.20
|
||||
version: 10.4.20(postcss@8.4.41)
|
||||
@@ -18225,7 +18231,7 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
execa: 5.1.1
|
||||
fast-glob: 3.3.2
|
||||
fast-xml-parser: 4.4.1
|
||||
fast-xml-parser: 4.5.0
|
||||
logkitty: 0.7.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -18236,7 +18242,7 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
execa: 5.1.1
|
||||
fast-glob: 3.3.2
|
||||
fast-xml-parser: 4.4.1
|
||||
fast-xml-parser: 4.5.0
|
||||
ora: 5.4.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
Reference in New Issue
Block a user