diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx index b1ab3df30d..28f6258c8f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx @@ -190,7 +190,8 @@ const Page = async (props) => { + rel="noopener noreferrer nofollow" + referrerPolicy="no-referrer"> {t("environments.settings.enterprise.request_30_day_trial_license")} diff --git a/apps/web/modules/auth/signup/page.tsx b/apps/web/modules/auth/signup/page.tsx index 4d947f15c7..d9b330971e 100644 --- a/apps/web/modules/auth/signup/page.tsx +++ b/apps/web/modules/auth/signup/page.tsx @@ -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(); diff --git a/packages/surveys/package.json b/packages/surveys/package.json index bc32b46645..fcdea6b51d 100644 --- a/packages/surveys/package.json +++ b/packages/surveys/package.json @@ -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" } } diff --git a/packages/surveys/src/components/buttons/BackButton.tsx b/packages/surveys/src/components/buttons/back-button.tsx similarity index 78% rename from packages/surveys/src/components/buttons/BackButton.tsx rename to packages/surveys/src/components/buttons/back-button.tsx index 9db10026d7..b3917ad124 100644 --- a/packages/surveys/src/components/buttons/BackButton.tsx +++ b/packages/surveys/src/components/buttons/back-button.tsx @@ -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 ( ); -}; +} diff --git a/packages/surveys/src/components/buttons/SubmitButton.tsx b/packages/surveys/src/components/buttons/submit-button.tsx similarity index 92% rename from packages/surveys/src/components/buttons/SubmitButton.tsx rename to packages/surveys/src/components/buttons/submit-button.tsx index 3d022db6e7..bdea30883d 100644 --- a/packages/surveys/src/components/buttons/SubmitButton.tsx +++ b/packages/surveys/src/components/buttons/submit-button.tsx @@ -1,4 +1,4 @@ -import { JSX } from "preact"; +import { type JSX } from "preact"; import { useCallback } from "preact/hooks"; interface SubmitButtonProps extends JSX.HTMLAttributes { @@ -7,7 +7,7 @@ interface SubmitButtonProps extends JSX.HTMLAttributes { 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")} ); -}; +} diff --git a/packages/surveys/src/components/general/LanguageSwitch.tsx b/packages/surveys/src/components/general/LanguageSwitch.tsx deleted file mode 100644 index 0ce89458d1..0000000000 --- a/packages/surveys/src/components/general/LanguageSwitch.tsx +++ /dev/null @@ -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 ( -
- - {showLanguageDropdown && ( -
- {surveyLanguages.map((surveyLanguage) => { - if (!surveyLanguage.enabled) return; - return ( - - ); - })} -
- )} -
- ); -}; diff --git a/packages/surveys/src/components/general/ResponseErrorComponent.tsx b/packages/surveys/src/components/general/ResponseErrorComponent.tsx deleted file mode 100644 index fd0bf77fc0..0000000000 --- a/packages/surveys/src/components/general/ResponseErrorComponent.tsx +++ /dev/null @@ -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 ( -
- - {"Your feedback is stuck :("} - -

- The servers cannot be reached at the moment. -
- Please retry now or try again later. -

-
-
- {questions.map((question, index) => { - const response = responseData[question.id]; - if (!response) return; - return ( -
- {`Question ${index + 1}`} - - {processResponseData(response)} - -
- ); - })} -
-
-
- onRetry()} /> -
-
- ); -}; diff --git a/packages/surveys/src/components/general/AutoCloseProgressBar.tsx b/packages/surveys/src/components/general/auto-close-progress-bar.tsx similarity index 60% rename from packages/surveys/src/components/general/AutoCloseProgressBar.tsx rename to packages/surveys/src/components/general/auto-close-progress-bar.tsx index 140fe22754..5440b689fd 100644 --- a/packages/surveys/src/components/general/AutoCloseProgressBar.tsx +++ b/packages/surveys/src/components/general/auto-close-progress-bar.tsx @@ -2,15 +2,15 @@ interface AutoCloseProgressBarProps { autoCloseTimeout: number; } -export const AutoCloseProgressBar = ({ autoCloseTimeout }: AutoCloseProgressBarProps) => { +export function AutoCloseProgressBar({ autoCloseTimeout }: AutoCloseProgressBarProps) { return (
+ animation: `shrink-width-to-zero ${autoCloseTimeout.toString()}s linear forwards`, + }} />
); -}; +} diff --git a/packages/surveys/src/components/general/CalEmbed.tsx b/packages/surveys/src/components/general/cal-embed.tsx similarity index 85% rename from packages/surveys/src/components/general/CalEmbed.tsx rename to packages/surveys/src/components/general/cal-embed.tsx index b33e117ac5..92d8f212d7 100644 --- a/packages/surveys/src/components/general/CalEmbed.tsx +++ b/packages/surveys/src/components/general/cal-embed.tsx @@ -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) => {
); -}; +} diff --git a/packages/surveys/src/components/general/EndingCard.tsx b/packages/surveys/src/components/general/ending-card.tsx similarity index 77% rename from packages/surveys/src/components/general/EndingCard.tsx rename to packages/surveys/src/components/general/ending-card.tsx index 324fd0c769..478cc44693 100644 --- a/packages/surveys/src/components/general/EndingCard.tsx +++ b/packages/surveys/src/components/general/ending-card.tsx @@ -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) ? ( ) : 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"> - + ); @@ -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 = ({
{isResponseSendingFinished ? ( <> - {endingCard.type === "endScreen" && (media || checkmark)} + {endingCard.type === "endScreen" && (media ?? checkmark)}
- {endingCard.type === "endScreen" && endingCard.buttonLabel && ( + {endingCard.type === "endScreen" && endingCard.buttonLabel ? (
- )} + ) : null}
) : ( @@ -147,4 +147,4 @@ export const EndingCard = ({
); -}; +} diff --git a/packages/surveys/src/components/general/FileInput.tsx b/packages/surveys/src/components/general/file-input.tsx similarity index 85% rename from packages/surveys/src/components/general/FileInput.tsx rename to packages/surveys/src/components/general/file-input.tsx index 333b4823c6..e998b91008 100644 --- a/packages/surveys/src/components/general/FileInput.tsx +++ b/packages/surveys/src/components/general/file-input.tsx @@ -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([]); 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) => { e.preventDefault(); e.stopPropagation(); - // @ts-expect-error + // @ts-expect-error -- TS does not recognize dataTransfer e.dataTransfer.dropEffect = "copy"; }; - const handleDrop = (e: JSXInternal.TargetedDragEvent) => { + const handleDrop = async (e: JSXInternal.TargetedDragEvent) => { 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) => { @@ -157,8 +158,7 @@ export const FileInput = ({ const uniqueHtmlFor = useMemo(() => `selectedFile-${htmlFor}`, [htmlFor]); return ( -
+
{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); + }}>
@@ -205,16 +207,16 @@ export const FileInput = ({
- {isUploading && ( + {isUploading ? (
- )} + ) : null}
); -}; +} diff --git a/packages/surveys/src/components/general/FormbricksBranding.tsx b/packages/surveys/src/components/general/formbricks-branding.tsx similarity index 75% rename from packages/surveys/src/components/general/FormbricksBranding.tsx rename to packages/surveys/src/components/general/formbricks-branding.tsx index 7caed1d65f..12bab8721e 100644 --- a/packages/surveys/src/components/general/FormbricksBranding.tsx +++ b/packages/surveys/src/components/general/formbricks-branding.tsx @@ -1,10 +1,10 @@ -export const FormbricksBranding = () => { +export function FormbricksBranding() { return ( + className="fb-my-2 fb-flex fb-justify-center" rel="noopener">

Powered by{" "} @@ -13,4 +13,4 @@ export const FormbricksBranding = () => {

); -}; +} diff --git a/packages/surveys/src/components/general/GlobeIcon.tsx b/packages/surveys/src/components/general/globe-icon.tsx similarity index 60% rename from packages/surveys/src/components/general/GlobeIcon.tsx rename to packages/surveys/src/components/general/globe-icon.tsx index 23d4291e4f..b28867b93b 100644 --- a/packages/surveys/src/components/general/GlobeIcon.tsx +++ b/packages/surveys/src/components/general/globe-icon.tsx @@ -2,21 +2,20 @@ interface GlobeIconProps { className?: string; } -export const GlobeIcon = ({ className }: GlobeIconProps) => { +export function GlobeIcon({ className }: GlobeIconProps) { return ( + strokeWidth="1" + strokeLinecap="round" + strokeLinejoin="round"> ); -}; +} diff --git a/packages/surveys/src/components/general/Headline.tsx b/packages/surveys/src/components/general/headline.tsx similarity index 78% rename from packages/surveys/src/components/general/Headline.tsx rename to packages/surveys/src/components/general/headline.tsx index aa55a1d89f..f4236e64d5 100644 --- a/packages/surveys/src/components/general/Headline.tsx +++ b/packages/surveys/src/components/general/headline.tsx @@ -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 ( ); -}; +} diff --git a/packages/surveys/src/components/general/HtmlBody.tsx b/packages/surveys/src/components/general/html-body.tsx similarity index 67% rename from packages/surveys/src/components/general/HtmlBody.tsx rename to packages/surveys/src/components/general/html-body.tsx index e124e4e1c3..b6d3f7d4af 100644 --- a/packages/surveys/src/components/general/HtmlBody.tsx +++ b/packages/surveys/src/components/general/html-body.tsx @@ -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" /> ); -}; +} diff --git a/packages/surveys/src/components/general/Input.tsx b/packages/surveys/src/components/general/input.tsx similarity index 89% rename from packages/surveys/src/components/general/Input.tsx rename to packages/surveys/src/components/general/input.tsx index 2608a72aca..3980379067 100644 --- a/packages/surveys/src/components/general/Input.tsx +++ b/packages/surveys/src/components/general/input.tsx @@ -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 { className?: string; } - export const Input = forwardRef(({ className, ...props }, ref) => { return ( 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 ( +
+ + {showLanguageDropdown ?
+ {surveyLanguages.map((surveyLanguage) => { + if (!surveyLanguage.enabled) return; + return ( + + ); + })} +
: null} +
+ ); +} diff --git a/packages/surveys/src/components/general/LoadingSpinner.tsx b/packages/surveys/src/components/general/loading-spinner.tsx similarity index 78% rename from packages/surveys/src/components/general/LoadingSpinner.tsx rename to packages/surveys/src/components/general/loading-spinner.tsx index 0861c569b4..d251360b0e 100644 --- a/packages/surveys/src/components/general/LoadingSpinner.tsx +++ b/packages/surveys/src/components/general/loading-spinner.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils"; -export const LoadingSpinner = ({ className }: { className?: string }) => { +export function LoadingSpinner({ className }: { className?: string }) { return (
{ cy="12" r="10" stroke="currentColor" - strokeWidth="4"> + strokeWidth="4" /> + 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" />
); -}; +} diff --git a/packages/surveys/src/components/general/ProgressBar.tsx b/packages/surveys/src/components/general/progress-bar.tsx similarity index 71% rename from packages/surveys/src/components/general/ProgressBar.tsx rename to packages/surveys/src/components/general/progress-bar.tsx index bd6b31997d..29c9cdef98 100644 --- a/packages/surveys/src/components/general/ProgressBar.tsx +++ b/packages/surveys/src/components/general/progress-bar.tsx @@ -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 ; -}; +} diff --git a/packages/surveys/src/components/general/Progress.tsx b/packages/surveys/src/components/general/progress.tsx similarity index 61% rename from packages/surveys/src/components/general/Progress.tsx rename to packages/surveys/src/components/general/progress.tsx index 2a35fd3bc3..affa037d23 100644 --- a/packages/surveys/src/components/general/Progress.tsx +++ b/packages/surveys/src/components/general/progress.tsx @@ -1,9 +1,10 @@ -export const Progress = ({ progress }: { progress: number }) => { +export function Progress({ progress }: { progress: number }) { return (
+ style={{ width: `${Math.floor(progress * 100).toString()}%` }} + />
); -}; +} diff --git a/packages/surveys/src/components/general/QuestionConditional.tsx b/packages/surveys/src/components/general/question-conditional.tsx similarity index 87% rename from packages/surveys/src/components/general/QuestionConditional.tsx rename to packages/surveys/src/components/general/question-conditional.tsx index c57c8df2ca..4c1fec2821 100644 --- a/packages/surveys/src/components/general/QuestionConditional.tsx +++ b/packages/surveys/src/components/general/question-conditional.tsx @@ -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; -}; +} diff --git a/packages/surveys/src/components/general/QuestionMedia.tsx b/packages/surveys/src/components/general/question-media.tsx similarity index 84% rename from packages/surveys/src/components/general/QuestionMedia.tsx rename to packages/surveys/src/components/general/question-media.tsx index abf7cbbc7e..9fe0442be8 100644 --- a/packages/surveys/src/components/general/QuestionMedia.tsx +++ b/packages/surveys/src/components/general/question-media.tsx @@ -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 (
- {isLoading && ( + {isLoading ? (
- )} - {imgUrl && ( + ) : null} + {imgUrl ? ( - )} - {videoUrlWithParams && ( + ) : null} + {videoUrlWithParams ? (
+ referrerpolicy="strict-origin-when-cross-origin" + />
- )} + ) : null} @@ -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"> @@ -84,4 +87,4 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
); -}; +} diff --git a/packages/surveys/src/components/general/RedirectCountdown.tsx b/packages/surveys/src/components/general/redirect-countdown.tsx similarity index 84% rename from packages/surveys/src/components/general/RedirectCountdown.tsx rename to packages/surveys/src/components/general/redirect-countdown.tsx index f975e4e333..00618532b8 100644 --- a/packages/surveys/src/components/general/RedirectCountdown.tsx +++ b/packages/surveys/src/components/general/redirect-countdown.tsx @@ -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
); -}; +} diff --git a/packages/surveys/src/components/general/response-error-component.tsx b/packages/surveys/src/components/general/response-error-component.tsx new file mode 100644 index 0000000000..b29f4b8c38 --- /dev/null +++ b/packages/surveys/src/components/general/response-error-component.tsx @@ -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 ( +
+ + Your feedback is stuck :( + +

+ The servers cannot be reached at the moment. +
+ Please retry now or try again later. +

+
+
+ {questions.map((question, index) => { + const response = responseData[question.id]; + if (!response) return; + return ( +
+ {`Question ${(index + 1).toString()}`} + + {processResponseData(response)} + +
+ ); + })} +
+
+
+ { + onRetry?.(); + }} + /> +
+
+ ); +} diff --git a/packages/surveys/src/components/general/Smileys.tsx b/packages/surveys/src/components/general/smileys.tsx similarity index 98% rename from packages/surveys/src/components/general/Smileys.tsx rename to packages/surveys/src/components/general/smileys.tsx index e779014690..c0201bea8c 100644 --- a/packages/surveys/src/components/general/Smileys.tsx +++ b/packages/surveys/src/components/general/smileys.tsx @@ -1,5 +1,4 @@ -import type { JSX } from "preact"; -import { FunctionComponent } from "preact"; +import type { FunctionComponent, JSX } from "preact"; export const TiredFace: FunctionComponent> = (props) => { return ( @@ -468,4 +467,4 @@ export const GrinningSquintingFace: FunctionComponent]; +export const icons = []; diff --git a/packages/surveys/src/components/general/Subheader.tsx b/packages/surveys/src/components/general/subheader.tsx similarity index 65% rename from packages/surveys/src/components/general/Subheader.tsx rename to packages/surveys/src/components/general/subheader.tsx index b6e1ed5a49..a14bfc2f92 100644 --- a/packages/surveys/src/components/general/Subheader.tsx +++ b/packages/surveys/src/components/general/subheader.tsx @@ -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 (

{ {subheader}

); -}; +} diff --git a/packages/surveys/src/components/general/SurveyCloseButton.tsx b/packages/surveys/src/components/general/survey-close-button.tsx similarity index 50% rename from packages/surveys/src/components/general/SurveyCloseButton.tsx rename to packages/surveys/src/components/general/survey-close-button.tsx index 21a055cacd..42b904f4a4 100644 --- a/packages/surveys/src/components/general/SurveyCloseButton.tsx +++ b/packages/surveys/src/components/general/survey-close-button.tsx @@ -1,16 +1,16 @@ interface SurveyCloseButtonProps { - onClose: () => void; + onClose?: () => void; } -export const SurveyCloseButton = ({ onClose }: SurveyCloseButtonProps) => { +export function SurveyCloseButton({ onClose }: SurveyCloseButtonProps) { return ( -
+
); -}; +} diff --git a/packages/surveys/src/components/general/SurveyInline.tsx b/packages/surveys/src/components/general/survey-inline.tsx similarity index 52% rename from packages/surveys/src/components/general/SurveyInline.tsx rename to packages/surveys/src/components/general/survey-inline.tsx index 2672dc97fd..9d6a884de5 100644 --- a/packages/surveys/src/components/general/SurveyInline.tsx +++ b/packages/surveys/src/components/general/survey-inline.tsx @@ -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 (
{
); -}; +} diff --git a/packages/surveys/src/components/general/SurveyModal.tsx b/packages/surveys/src/components/general/survey-modal.tsx similarity index 82% rename from packages/surveys/src/components/general/SurveyModal.tsx rename to packages/surveys/src/components/general/survey-modal.tsx index 8a09eb4372..b483cd677d 100644 --- a/packages/surveys/src/components/general/SurveyModal.tsx +++ b/packages/surveys/src/components/general/survey-modal.tsx @@ -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 (
@@ -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 = ({
); -}; +} diff --git a/packages/surveys/src/components/general/Survey.tsx b/packages/surveys/src/components/general/survey.tsx similarity index 77% rename from packages/surveys/src/components/general/Survey.tsx rename to packages/surveys/src/components/general/survey.tsx index f5f4fb63cc..eac9015dc6 100644 --- a/packages/surveys/src/components/general/Survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -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(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(hiddenFieldsRecord ?? {}); const [_variableStack, setVariableStack] = useState([]); const [currentVariables, setCurrentVariables] = useState(() => { - return localSurvey.variables.reduce((acc, variable) => { + return localSurvey.variables.reduce((acc, variable) => { acc[variable.id] = variable.value; return acc; - }, {} as TResponseVariables); + }, {}); }); const [ttc, setTtc] = useState({}); @@ -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(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) && (
- {isBrandingEnabled && } - {showProgressBar && } + {isBrandingEnabled ? : null} + {showProgressBar ? : null}
@@ -452,17 +449,15 @@ export const Survey = ({ }; return ( - <> - - + ); -}; +} diff --git a/packages/surveys/src/components/general/WelcomeCard.tsx b/packages/surveys/src/components/general/welcome-card.tsx similarity index 77% rename from packages/surveys/src/components/general/WelcomeCard.tsx rename to packages/surveys/src/components/general/welcome-card.tsx index d075584b1d..2e21e7996b 100644 --- a/packages/surveys/src/components/general/WelcomeCard.tsx +++ b/packages/surveys/src/components/general/welcome-card.tsx @@ -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 (
{ width="16" height="16" fill="currentColor" - class="bi bi-stopwatch" + className="bi bi-stopwatch" viewBox="0 0 16 16">
); -}; +} -const UsersIcon = () => { +function UsersIcon() { return (
{ viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" - class="fb-h-4 fb-w-4"> + className="fb-h-4 fb-w-4"> {
); -}; +} -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 (
- {fileUrl && ( + {fileUrl ? ( Company Logo - )} + ) : null}
-
e.key === "Enter" && e.preventDefault()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + } + }} />
- {timeToFinish && !showResponseCount ? (
@@ -177,22 +178,26 @@ export const WelcomeCard = ({ Takes {calculateTimeToComplete()}

- ) : showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? ( + ) : null} + {showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (

- {`${responseCount} people responded`} + {`${responseCount.toString()} people responded`}

- ) : timeToFinish && showResponseCount ? ( + ) : null} + {timeToFinish && showResponseCount ? (

Takes {calculateTimeToComplete()} - {responseCount && responseCount > 3 ? `â‹… ${responseCount} people responded` : ""} + + {responseCount && responseCount > 3 ? `â‹… ${responseCount.toString()} people responded` : ""} +

) : null}
); -}; +} diff --git a/packages/surveys/src/components/questions/AddressQuestion.tsx b/packages/surveys/src/components/questions/address-question.tsx similarity index 79% rename from packages/surveys/src/components/questions/AddressQuestion.tsx rename to packages/surveys/src/components/questions/address-question.tsx index 98f2e210e5..a1ea6a33a5 100644 --- a/packages/surveys/src/components/questions/AddressQuestion.tsx +++ b/packages/surveys/src/components/questions/address-question.tsx @@ -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(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 = ({
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} -
+
{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={() => {}} /> -
+
{!isFirstQuestion && ( ); -}; +} diff --git a/packages/surveys/src/components/questions/CalQuestion.tsx b/packages/surveys/src/components/questions/cal-question.tsx similarity index 73% rename from packages/surveys/src/components/questions/CalQuestion.tsx rename to packages/surveys/src/components/questions/cal-question.tsx index 52450ef3d4..4b15e41903 100644 --- a/packages/surveys/src/components/questions/CalQuestion.tsx +++ b/packages/surveys/src/components/questions/cal-question.tsx @@ -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">
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} - <> - {errorMessage && {errorMessage}} - - + {errorMessage ? {errorMessage} : null} +
@@ -95,7 +94,7 @@ export const CalQuestion = ({ tabIndex={isCurrent ? 0 : -1} /> )} -
+
{!isFirstQuestion && ( ); -}; +} diff --git a/packages/surveys/src/components/questions/ConsentQuestion.tsx b/packages/surveys/src/components/questions/consent-question.tsx similarity index 87% rename from packages/surveys/src/components/questions/ConsentQuestion.tsx rename to packages/surveys/src/components/questions/consent-question.tsx index bde52a2857..4576978ae0 100644 --- a/packages/surveys/src/components/questions/ConsentQuestion.tsx +++ b/packages/surveys/src/components/questions/consent-question.tsx @@ -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 = ({ }}>
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} -
+
{!isFirstQuestion && ( ); -}; +} diff --git a/packages/surveys/src/components/questions/ContactInfoQuestion.tsx b/packages/surveys/src/components/questions/contact-info-question.tsx similarity index 79% rename from packages/surveys/src/components/questions/ContactInfoQuestion.tsx rename to packages/surveys/src/components/questions/contact-info-question.tsx index a1d7f890ab..5b20a2973b 100644 --- a/packages/surveys/src/components/questions/ContactInfoQuestion.tsx +++ b/packages/surveys/src/components/questions/contact-info-question.tsx @@ -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(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 = ({
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} -
+
{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 && ( ); -}; +} diff --git a/packages/surveys/src/components/questions/CTAQuestion.tsx b/packages/surveys/src/components/questions/cta-question.tsx similarity index 83% rename from packages/surveys/src/components/questions/CTAQuestion.tsx rename to packages/surveys/src/components/questions/cta-question.tsx index dbb8d388be..cf0bbfcfe4 100644 --- a/packages/surveys/src/components/questions/CTAQuestion.tsx +++ b/packages/surveys/src/components/questions/cta-question.tsx @@ -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 = ({
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} { 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 = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/DateQuestion.tsx b/packages/surveys/src/components/questions/date-question.tsx similarity index 78% rename from packages/surveys/src/components/questions/DateQuestion.tsx rename to packages/surveys/src/components/questions/date-question.tsx index e7a92ac9ea..540b4a4a7b 100644 --- a/packages/surveys/src/components/questions/DateQuestion.tsx +++ b/packages/surveys/src/components/questions/date-question.tsx @@ -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 = () => ( - - - - - - - - - - - - -); +function CalendarIcon() { + return ( + + + + + + + + + + + + + ); +} -const CalendarCheckIcon = () => ( - - - - - - - -); +function CalendarCheckIcon() { + return ( + + + + + + + + ); +} -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">
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} -
+
{errorMessage}
{!datePickerOpen && (
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 = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/FileUploadQuestion.tsx b/packages/surveys/src/components/questions/file-upload-question.tsx similarity index 74% rename from packages/surveys/src/components/questions/FileUploadQuestion.tsx rename to packages/surveys/src/components/questions/file-upload-question.tsx index b4ae6360c3..7917ce3425 100644 --- a/packages/surveys/src/components/questions/FileUploadQuestion.tsx +++ b/packages/surveys/src/components/questions/file-upload-question.tsx @@ -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">
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null}
@@ -122,4 +122,4 @@ export const FileUploadQuestion = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/MatrixQuestion.tsx b/packages/surveys/src/components/questions/matrix-question.tsx similarity index 82% rename from packages/surveys/src/components/questions/MatrixQuestion.tsx rename to packages/surveys/src/components/questions/matrix-question.tsx index 30edd007db..fb2063d6a2 100644 --- a/packages/surveys/src/components/questions/MatrixQuestion.tsx +++ b/packages/surveys/src/components/questions/matrix-question.tsx @@ -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) => { + (e: JSX.TargetedEvent) => { e.preventDefault(); const updatedTtc = getUpdatedTtc(ttc, question.id, performance.now() - startTime); setTtc(updatedTtc); @@ -119,7 +119,9 @@ export const MatrixQuestion = ({
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} - + {columnsHeaders} {questionRows.map((row, rowIndex) => ( // Table rows - + @@ -148,15 +150,15 @@ export const MatrixQuestion = ({ {question.columns.map((column, columnIndex) => ( + 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 = ({ {}} tabIndex={isCurrent ? 0 : -1} /> {!isFirstQuestion && ( @@ -212,4 +213,4 @@ export const MatrixQuestion = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/MultipleChoiceMultiQuestion.tsx b/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx similarity index 87% rename from packages/surveys/src/components/questions/MultipleChoiceMultiQuestion.tsx rename to packages/surveys/src/components/questions/multiple-choice-multi-question.tsx index ab817ff1ae..bb26e1e984 100644 --- a/packages/surveys/src/components/questions/MultipleChoiceMultiQuestion.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-multi-question.tsx @@ -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">
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} { - 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 = ({ ); })} - {otherOption && ( + {otherOption ? ( - )} + ) : null}
@@ -302,4 +307,4 @@ export const MultipleChoiceMultiQuestion = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/MultipleChoiceSingleQuestion.tsx b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx similarity index 88% rename from packages/surveys/src/components/questions/MultipleChoiceSingleQuestion.tsx rename to packages/surveys/src/components/questions/multiple-choice-single-question.tsx index 51864d16fb..03099c3f7c 100644 --- a/packages/surveys/src/components/questions/MultipleChoiceSingleQuestion.tsx +++ b/packages/surveys/src/components/questions/multiple-choice-single-question.tsx @@ -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(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">
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} {getLocalizedValue(choice.label, languageCode)} @@ -173,7 +176,7 @@ export const MultipleChoiceSingleQuestion = ({ ); })} - {otherOption && ( + {otherOption ? ( - {otherSelected && ( + {otherSelected ? ( 0 + ? getLocalizedValue(question.otherOptionPlaceholder, languageCode) + : "Please specify" } required={question.required} aria-labelledby={`${otherOption.id}-label`} /> - )} + ) : null} - )} + ) : null}
@@ -259,4 +264,4 @@ export const MultipleChoiceSingleQuestion = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/NPSQuestion.tsx b/packages/surveys/src/components/questions/nps-question.tsx similarity index 82% rename from packages/surveys/src/components/questions/NPSQuestion.tsx rename to packages/surveys/src/components/questions/nps-question.tsx index 2d388e09cd..bf0db74b74 100644 --- a/packages/surveys/src/components/questions/NPSQuestion.tsx +++ b/packages/surveys/src/components/questions/nps-question.tsx @@ -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 = ({ }}>
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} 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 ? (
- )} + ) : null} handleClick(number)} + onClick={() => { + handleClick(number); + }} required={question.required} tabIndex={-1} /> @@ -164,4 +174,4 @@ export const NPSQuestion = ({
); -}; +} diff --git a/packages/surveys/src/components/questions/OpenTextQuestion.tsx b/packages/surveys/src/components/questions/open-text-question.tsx similarity index 82% rename from packages/surveys/src/components/questions/OpenTextQuestion.tsx rename to packages/surveys/src/components/questions/open-text-question.tsx index 2f8eb37519..4e811440d1 100644 --- a/packages/surveys/src/components/questions/OpenTextQuestion.tsx +++ b/packages/surveys/src/components/questions/open-text-question.tsx @@ -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">
- {isMediaAvailable && } + {isMediaAvailable ? ( + + ) : null} } - 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 = ({