mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 21:32:02 -06:00
chore: Remove lib dependency from survey package (#4767)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
committed by
GitHub
parent
40d54d60d4
commit
0e898db710
@@ -1,7 +1,7 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type Prisma, PrismaClient } from "@prisma/client";
|
||||
import { exec } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { type Prisma, PrismaClient } from "@prisma/client";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => {
|
||||
const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => {
|
||||
// return undefined if hex is undefined, this is important for adding the default values to the CSS variables
|
||||
// TODO: find a better way to handle this
|
||||
if (!hex || hex === "") return undefined;
|
||||
@@ -17,34 +17,6 @@ export const hexToRGBA = (hex: string | undefined, opacity: number): string | un
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
|
||||
export const lightenDarkenColor = (hexColor: string, magnitude: number): string => {
|
||||
hexColor = hexColor.replace(`#`, ``);
|
||||
|
||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||
if (hexColor.length === 3) {
|
||||
hexColor = hexColor
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.join("");
|
||||
}
|
||||
|
||||
if (hexColor.length === 6) {
|
||||
let decimalColor = parseInt(hexColor, 16);
|
||||
let r = (decimalColor >> 16) + magnitude;
|
||||
r = Math.max(0, Math.min(255, r)); // Clamp value between 0 and 255
|
||||
let g = ((decimalColor >> 8) & 0x00ff) + magnitude;
|
||||
g = Math.max(0, Math.min(255, g)); // Clamp value between 0 and 255
|
||||
let b = (decimalColor & 0x0000ff) + magnitude;
|
||||
b = Math.max(0, Math.min(255, b)); // Clamp value between 0 and 255
|
||||
|
||||
// Convert back to hex and return
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
} else {
|
||||
// Return the original color if it's neither 3 nor 6 characters
|
||||
return hexColor;
|
||||
}
|
||||
};
|
||||
|
||||
export const mixColor = (hexColor: string, mixWithHex: string, weight: number): string => {
|
||||
// Convert both colors to RGBA format
|
||||
const color1 = hexToRGBA(hexColor, 1) || "";
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
const monthNames = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
const getOrdinalSuffix = (day: number) => {
|
||||
const suffixes = ["th", "st", "nd", "rd"];
|
||||
const relevantDigits = day < 30 ? day % 20 : day % 30;
|
||||
return suffixes[relevantDigits <= 3 ? relevantDigits : 0];
|
||||
};
|
||||
|
||||
// Helper function to calculate difference in days between two dates
|
||||
export const diffInDays = (date1: Date, date2: Date) => {
|
||||
@@ -19,42 +10,12 @@ export const diffInDays = (date1: Date, date2: Date) => {
|
||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
// Helper function to get the month name
|
||||
export const getMonthName = (monthIndex: number) => {
|
||||
return monthNames[monthIndex];
|
||||
};
|
||||
|
||||
export const formatDateWithOrdinal = (date: Date): string => {
|
||||
const getOrdinalSuffix = (day: number) => {
|
||||
const suffixes = ["th", "st", "nd", "rd"];
|
||||
const relevantDigits = day < 30 ? day % 20 : day % 30;
|
||||
return suffixes[relevantDigits <= 3 ? relevantDigits : 0];
|
||||
};
|
||||
|
||||
const dayOfWeekNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
const dayOfWeek = dayOfWeekNames[date.getDay()];
|
||||
export const formatDateWithOrdinal = (date: Date, locale: string = "en-US"): string => {
|
||||
const dayOfWeek = new Intl.DateTimeFormat(locale, { weekday: "long" }).format(date);
|
||||
const day = date.getDate();
|
||||
const monthIndex = date.getMonth();
|
||||
const month = new Intl.DateTimeFormat(locale, { month: "long" }).format(date);
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${dayOfWeek}, ${monthNames[monthIndex]} ${day}${getOrdinalSuffix(day)}, ${year}`;
|
||||
};
|
||||
|
||||
// Helper function to format the date with an ordinal suffix
|
||||
export const getOrdinalDate = (date: number) => {
|
||||
const j = date % 10,
|
||||
k = date % 100;
|
||||
if (j === 1 && k !== 11) {
|
||||
return date + "st";
|
||||
}
|
||||
if (j === 2 && k !== 12) {
|
||||
return date + "nd";
|
||||
}
|
||||
if (j === 3 && k !== 13) {
|
||||
return date + "rd";
|
||||
}
|
||||
return date + "th";
|
||||
return `${dayOfWeek}, ${month} ${day}${getOrdinalSuffix(day)}, ${year}`;
|
||||
};
|
||||
|
||||
export const isValidDateString = (value: string) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const checkForYoutubeUrl = (url: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForVimeoUrl = (url: string): boolean => {
|
||||
const checkForVimeoUrl = (url: string): boolean => {
|
||||
try {
|
||||
const vimeoUrl = new URL(url);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const checkForVimeoUrl = (url: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForLoomUrl = (url: string): boolean => {
|
||||
const checkForLoomUrl = (url: string): boolean => {
|
||||
try {
|
||||
const loomUrl = new URL(url);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable no-console -- debugging*/
|
||||
import React, { type JSX, useEffect, useRef, useState } from "react";
|
||||
import { Modal } from "react-native";
|
||||
import { WebView, type WebViewMessageEvent } from "react-native-webview";
|
||||
import { RNConfig } from "@/lib/common/config";
|
||||
import { Logger } from "@/lib/common/logger";
|
||||
import { filterSurveys, getLanguageCode, getStyling } from "@/lib/common/utils";
|
||||
import { SurveyStore } from "@/lib/survey/store";
|
||||
import { type TEnvironmentStateSurvey, type TUserState, ZJsRNWebViewOnMessageData } from "@/types/config";
|
||||
import type { SurveyContainerProps } from "@/types/survey";
|
||||
import React, { type JSX, useEffect, useRef, useState } from "react";
|
||||
import { Modal } from "react-native";
|
||||
import { WebView, type WebViewMessageEvent } from "react-native-webview";
|
||||
|
||||
const appConfig = RNConfig.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
|
||||
@@ -4,9 +4,9 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { replaceRecallInfo } from "@/lib/recall";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
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";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getOriginalFileNameFromUrl } from "@/lib/storage";
|
||||
import { isFulfilled, isRejected } from "@/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { type JSXInternal } from "preact/src/jsx";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { isFulfilled, isRejected } from "@formbricks/lib/utils/promises";
|
||||
import { type TAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GlobeIcon } from "@/components/general/globe-icon";
|
||||
import { useClickOutside } from "@/lib/utils";
|
||||
import { useClickOutside } from "@/lib/use-click-outside-hook";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { getLanguageLabel } from "@formbricks/lib/i18n/utils";
|
||||
import { type TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
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";
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { checkForLoomUrl, checkForVimeoUrl, checkForYoutubeUrl, convertToEmbedUrl } from "@/lib/video-upload";
|
||||
import { useState } from "preact/hooks";
|
||||
import {
|
||||
checkForLoomUrl,
|
||||
checkForVimeoUrl,
|
||||
checkForYoutubeUrl,
|
||||
convertToEmbedUrl,
|
||||
} from "@formbricks/lib/utils/videoUpload";
|
||||
|
||||
//Function to add extra params to videoUrls in order to reduce video controls
|
||||
const getVideoUrlWithParams = (videoUrl: string): string => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { processResponseData } from "@formbricks/lib/responses";
|
||||
import { processResponseData } from "@/lib/response";
|
||||
import { type TResponseData } from "@formbricks/types/responses";
|
||||
import { type TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ import { WelcomeCard } from "@/components/general/welcome-card";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/auto-close-wrapper";
|
||||
import { StackedCardsContainer } from "@/components/wrappers/stacked-cards-container";
|
||||
import { ApiClient } from "@/lib/api-client";
|
||||
import { evaluateLogic, performActions } from "@/lib/logic";
|
||||
import { parseRecallInformation } from "@/lib/recall";
|
||||
import { ResponseQueue } from "@/lib/response-queue";
|
||||
import { SurveyState } from "@/lib/survey-state";
|
||||
import { cn, getDefaultLanguageCode } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { type JSX, useCallback } from "react";
|
||||
import { evaluateLogic, performActions } from "@formbricks/lib/surveyLogic/utils";
|
||||
import { SurveyContainerProps } from "@formbricks/types/formbricks-surveys";
|
||||
import { type TJsEnvironmentStateSurvey, TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import type {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SubmitButton } from "@/components/buttons/submit-button";
|
||||
import { ScrollableContainer } from "@/components/wrappers/scrollable-container";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { replaceRecallInfo } from "@/lib/recall";
|
||||
import { calculateElementIdx } from "@/lib/utils";
|
||||
import { useEffect } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
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";
|
||||
|
||||
@@ -5,10 +5,10 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
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 { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyAddressQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TSurveyCalQuestion, type TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyConsentQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyContactInfoQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyCTAQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ 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 { getMonthName, getOrdinalDate } from "@/lib/date-time";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
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 { DatePickerProps } from "react-date-picker";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getMonthName, getOrdinalDate } from "@formbricks/lib/utils/datetime";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyDateQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import "../../styles/date-picker.css";
|
||||
|
||||
@@ -2,9 +2,9 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||
|
||||
@@ -4,11 +4,11 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { getShuffledRowIndices } from "@/lib/utils";
|
||||
import { type JSX } from "preact";
|
||||
import { useCallback, useMemo, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TI18nString, TSurveyMatrixQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
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 { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
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 { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyMultipleChoiceQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyNPSQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { type RefObject } from "preact";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyOpenTextQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getOriginalFileNameFromUrl } from "@/lib/storage";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyPictureSelectionQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn, getShuffledChoicesIds } from "@/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useCallback, useMemo, useState } from "preact/hooks";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type {
|
||||
TSurveyQuestionChoice,
|
||||
|
||||
@@ -3,11 +3,11 @@ 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 { getLocalizedValue } from "@/lib/i18n";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||
import type { TSurveyQuestionId, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { type TPlacement } from "@formbricks/types/common";
|
||||
|
||||
interface SurveyContainerProps {
|
||||
|
||||
53
packages/surveys/src/lib/color.ts
Normal file
53
packages/surveys/src/lib/color.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => {
|
||||
// return undefined if hex is undefined, this is important for adding the default values to the CSS variables
|
||||
// TODO: find a better way to handle this
|
||||
if (!hex || hex === "") return undefined;
|
||||
|
||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
||||
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||
hex = hex.replace(shorthandRegex, (_, r, g, b) => r + r + g + g + b + b);
|
||||
|
||||
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (!result) return "";
|
||||
|
||||
let r = parseInt(result[1], 16);
|
||||
let g = parseInt(result[2], 16);
|
||||
let b = parseInt(result[3], 16);
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
|
||||
export const mixColor = (hexColor: string, mixWithHex: string, weight: number): string => {
|
||||
// Convert both colors to RGBA format
|
||||
const color1 = hexToRGBA(hexColor, 1) || "";
|
||||
const color2 = hexToRGBA(mixWithHex, 1) || "";
|
||||
|
||||
// Extract RGBA values
|
||||
const [r1, g1, b1] = color1.match(/\d+/g)?.map(Number) || [0, 0, 0];
|
||||
const [r2, g2, b2] = color2.match(/\d+/g)?.map(Number) || [0, 0, 0];
|
||||
|
||||
// Mix the colors
|
||||
const r = Math.round(r1 * (1 - weight) + r2 * weight);
|
||||
const g = Math.round(g1 * (1 - weight) + g2 * weight);
|
||||
const b = Math.round(b1 * (1 - weight) + b2 * weight);
|
||||
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
};
|
||||
|
||||
export const isLight = (color: string) => {
|
||||
let r: number | undefined, g: number | undefined, b: number | undefined;
|
||||
|
||||
if (color.length === 4) {
|
||||
r = parseInt(color[1] + color[1], 16);
|
||||
g = parseInt(color[2] + color[2], 16);
|
||||
b = parseInt(color[3] + color[3], 16);
|
||||
} else if (color.length === 7) {
|
||||
r = parseInt(color[1] + color[2], 16);
|
||||
g = parseInt(color[3] + color[4], 16);
|
||||
b = parseInt(color[5] + color[6], 16);
|
||||
}
|
||||
if (r === undefined || g === undefined || b === undefined) {
|
||||
throw new Error("Invalid color");
|
||||
}
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 128;
|
||||
};
|
||||
48
packages/surveys/src/lib/date-time.ts
Normal file
48
packages/surveys/src/lib/date-time.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Helper function to get the month name
|
||||
export const getMonthName = (monthIndex: number, locale: string = "en-US") => {
|
||||
if (monthIndex < 0 || monthIndex > 11) {
|
||||
throw new Error("Month index must be between 0 and 11");
|
||||
}
|
||||
return new Intl.DateTimeFormat(locale, { month: "long" }).format(new Date(2000, monthIndex, 1));
|
||||
};
|
||||
|
||||
// Helper function to format the date with an ordinal suffix
|
||||
export const getOrdinalDate = (date: number) => {
|
||||
const j = date % 10,
|
||||
k = date % 100;
|
||||
if (j === 1 && k !== 11) {
|
||||
return date + "st";
|
||||
}
|
||||
if (j === 2 && k !== 12) {
|
||||
return date + "nd";
|
||||
}
|
||||
if (j === 3 && k !== 13) {
|
||||
return date + "rd";
|
||||
}
|
||||
return date + "th";
|
||||
};
|
||||
|
||||
export const isValidDateString = (value: string) => {
|
||||
const regex = /^(?:\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4})$/;
|
||||
|
||||
if (!regex.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
return !isNaN(date.getTime());
|
||||
};
|
||||
|
||||
const getOrdinalSuffix = (day: number): string => {
|
||||
const suffixes = ["th", "st", "nd", "rd"];
|
||||
const relevantDigits = day < 30 ? day % 20 : day % 30;
|
||||
return suffixes[relevantDigits <= 3 ? relevantDigits : 0];
|
||||
};
|
||||
|
||||
export const formatDateWithOrdinal = (date: Date, locale: string = "en-US"): string => {
|
||||
const dayOfWeek = new Intl.DateTimeFormat(locale, { weekday: "long" }).format(date);
|
||||
const day = date.getDate();
|
||||
const month = new Intl.DateTimeFormat(locale, { month: "long" }).format(date);
|
||||
const year = date.getFullYear();
|
||||
return `${dayOfWeek}, ${month} ${day}${getOrdinalSuffix(day)}, ${year}`;
|
||||
};
|
||||
19
packages/surveys/src/lib/i18n.ts
Normal file
19
packages/surveys/src/lib/i18n.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { TI18nString } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Type guard to check if an object is an I18nString
|
||||
const isI18nObject = (obj: any): obj is TI18nString => {
|
||||
return typeof obj === "object" && obj !== null && Object.keys(obj).includes("default");
|
||||
};
|
||||
|
||||
export const getLocalizedValue = (value: TI18nString | undefined, languageId: string): string => {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
if (isI18nObject(value)) {
|
||||
if (value[languageId]) {
|
||||
return value[languageId];
|
||||
}
|
||||
return value.default;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
473
packages/surveys/src/lib/logic.ts
Normal file
473
packages/surveys/src/lib/logic.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
|
||||
import {
|
||||
TActionCalculate,
|
||||
TConditionGroup,
|
||||
TSingleCondition,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
|
||||
const getVariableValue = (
|
||||
variables: TSurveyVariable[],
|
||||
variableId: string,
|
||||
variablesData: TResponseVariables
|
||||
) => {
|
||||
const variable = variables.find((v) => v.id === variableId);
|
||||
if (!variable) return undefined;
|
||||
const variableValue = variablesData[variableId];
|
||||
return variable.type === "number" ? Number(variableValue) || 0 : variableValue || "";
|
||||
};
|
||||
|
||||
type TCondition = TSingleCondition | TConditionGroup;
|
||||
|
||||
export const isConditionGroup = (condition: TCondition): condition is TConditionGroup => {
|
||||
return (condition as TConditionGroup).connector !== undefined;
|
||||
};
|
||||
|
||||
export const evaluateLogic = (
|
||||
localSurvey: TJsEnvironmentStateSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
conditions: TConditionGroup,
|
||||
selectedLanguage: string
|
||||
): boolean => {
|
||||
const evaluateConditionGroup = (group: TConditionGroup): boolean => {
|
||||
const results = group.conditions.map((condition) => {
|
||||
if (isConditionGroup(condition)) {
|
||||
return evaluateConditionGroup(condition);
|
||||
} else {
|
||||
return evaluateSingleCondition(localSurvey, data, variablesData, condition, selectedLanguage);
|
||||
}
|
||||
});
|
||||
|
||||
return group.connector === "or" ? results.some((r) => r) : results.every((r) => r);
|
||||
};
|
||||
|
||||
return evaluateConditionGroup(conditions);
|
||||
};
|
||||
|
||||
export const performActions = (
|
||||
survey: TJsEnvironmentStateSurvey,
|
||||
actions: TSurveyLogicAction[],
|
||||
data: TResponseData,
|
||||
calculationResults: TResponseVariables
|
||||
): {
|
||||
jumpTarget: string | undefined;
|
||||
requiredQuestionIds: string[];
|
||||
calculations: TResponseVariables;
|
||||
} => {
|
||||
let jumpTarget: string | undefined;
|
||||
const requiredQuestionIds: string[] = [];
|
||||
const calculations: TResponseVariables = { ...calculationResults };
|
||||
|
||||
actions.forEach((action) => {
|
||||
switch (action.objective) {
|
||||
case "calculate":
|
||||
const result = performCalculation(survey, action, data, calculations);
|
||||
if (result !== undefined) calculations[action.variableId] = result;
|
||||
break;
|
||||
case "requireAnswer":
|
||||
requiredQuestionIds.push(action.target);
|
||||
break;
|
||||
case "jumpToQuestion":
|
||||
if (!jumpTarget) {
|
||||
jumpTarget = action.target;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return { jumpTarget, requiredQuestionIds, calculations };
|
||||
};
|
||||
|
||||
const getLeftOperandValue = (
|
||||
localSurvey: TJsEnvironmentStateSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
leftOperand: TSingleCondition["leftOperand"],
|
||||
selectedLanguage: string
|
||||
) => {
|
||||
switch (leftOperand.type) {
|
||||
case "question":
|
||||
const currentQuestion = localSurvey.questions.find((q) => q.id === leftOperand.value);
|
||||
if (!currentQuestion) return undefined;
|
||||
|
||||
const responseValue = data[leftOperand.value];
|
||||
|
||||
if (currentQuestion.type === "openText" && currentQuestion.inputType === "number") {
|
||||
return Number(responseValue) || undefined;
|
||||
}
|
||||
|
||||
if (currentQuestion.type === "multipleChoiceSingle" || currentQuestion.type === "multipleChoiceMulti") {
|
||||
const isOthersEnabled = currentQuestion.choices.at(-1)?.id === "other";
|
||||
|
||||
if (typeof responseValue === "string") {
|
||||
const choice = currentQuestion.choices.find((choice) => {
|
||||
return getLocalizedValue(choice.label, selectedLanguage) === responseValue;
|
||||
});
|
||||
|
||||
if (!choice) {
|
||||
if (isOthersEnabled) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return choice.id;
|
||||
} else if (Array.isArray(responseValue)) {
|
||||
let choices: string[] = [];
|
||||
responseValue.forEach((value) => {
|
||||
const foundChoice = currentQuestion.choices.find((choice) => {
|
||||
return getLocalizedValue(choice.label, selectedLanguage) === value;
|
||||
});
|
||||
|
||||
if (foundChoice) {
|
||||
choices.push(foundChoice.id);
|
||||
} else if (isOthersEnabled) {
|
||||
choices.push("other");
|
||||
}
|
||||
});
|
||||
if (choices) {
|
||||
return Array.from(new Set(choices));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data[leftOperand.value];
|
||||
case "variable":
|
||||
const variables = localSurvey.variables || [];
|
||||
return getVariableValue(variables, leftOperand.value, variablesData);
|
||||
case "hiddenField":
|
||||
return data[leftOperand.value];
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getRightOperandValue = (
|
||||
localSurvey: TJsEnvironmentStateSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
rightOperand: TSingleCondition["rightOperand"]
|
||||
) => {
|
||||
if (!rightOperand) return undefined;
|
||||
|
||||
switch (rightOperand.type) {
|
||||
case "question":
|
||||
return data[rightOperand.value];
|
||||
case "variable":
|
||||
const variables = localSurvey.variables || [];
|
||||
return getVariableValue(variables, rightOperand.value, variablesData);
|
||||
case "hiddenField":
|
||||
return data[rightOperand.value];
|
||||
case "static":
|
||||
return rightOperand.value;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const evaluateSingleCondition = (
|
||||
localSurvey: TJsEnvironmentStateSurvey,
|
||||
data: TResponseData,
|
||||
variablesData: TResponseVariables,
|
||||
condition: TSingleCondition,
|
||||
selectedLanguage: string
|
||||
): boolean => {
|
||||
try {
|
||||
let leftValue = getLeftOperandValue(
|
||||
localSurvey,
|
||||
data,
|
||||
variablesData,
|
||||
condition.leftOperand,
|
||||
selectedLanguage
|
||||
);
|
||||
let rightValue = condition.rightOperand
|
||||
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
|
||||
: undefined;
|
||||
|
||||
let leftField: TSurveyQuestion | TSurveyVariable | string;
|
||||
|
||||
if (condition.leftOperand?.type === "question") {
|
||||
leftField = localSurvey.questions.find((q) => q.id === condition.leftOperand?.value) as TSurveyQuestion;
|
||||
} else if (condition.leftOperand?.type === "variable") {
|
||||
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable;
|
||||
} else if (condition.leftOperand?.type === "hiddenField") {
|
||||
leftField = condition.leftOperand.value as string;
|
||||
} else {
|
||||
leftField = "";
|
||||
}
|
||||
|
||||
let rightField: TSurveyQuestion | TSurveyVariable | string;
|
||||
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
rightField = localSurvey.questions.find(
|
||||
(q) => q.id === condition.rightOperand?.value
|
||||
) as TSurveyQuestion;
|
||||
} else if (condition.rightOperand?.type === "variable") {
|
||||
rightField = localSurvey.variables.find(
|
||||
(v) => v.id === condition.rightOperand?.value
|
||||
) as TSurveyVariable;
|
||||
} else if (condition.rightOperand?.type === "hiddenField") {
|
||||
rightField = condition.rightOperand.value as string;
|
||||
} else {
|
||||
rightField = "";
|
||||
}
|
||||
|
||||
if (
|
||||
condition.leftOperand.type === "variable" &&
|
||||
(leftField as TSurveyVariable).type === "number" &&
|
||||
condition.rightOperand?.type === "hiddenField"
|
||||
) {
|
||||
rightValue = Number(rightValue as string);
|
||||
}
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
if (condition.leftOperand.type === "question") {
|
||||
if (
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
// when left value is of date question and right value is string
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
||||
return rightValue.includes(leftValue as string);
|
||||
} else return false;
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(Array.isArray(leftValue) &&
|
||||
leftValue.length === 1 &&
|
||||
typeof rightValue === "string" &&
|
||||
leftValue.includes(rightValue)) ||
|
||||
leftValue === rightValue
|
||||
);
|
||||
case "doesNotEqual":
|
||||
// when left value is of picture selection question and right value is its option
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.PictureSelection &&
|
||||
Array.isArray(leftValue) &&
|
||||
leftValue.length > 0 &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return !leftValue.includes(rightValue);
|
||||
}
|
||||
|
||||
// when left value is of date question and right value is string
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
||||
}
|
||||
|
||||
// when left value is of openText, hiddenField, variable and right value is of multichoice
|
||||
if (condition.rightOperand?.type === "question") {
|
||||
if ((rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) {
|
||||
if (Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.length === 1) {
|
||||
return !rightValue.includes(leftValue as string);
|
||||
} else return false;
|
||||
} else if (
|
||||
(rightField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.Date &&
|
||||
typeof leftValue === "string" &&
|
||||
typeof rightValue === "string"
|
||||
) {
|
||||
return new Date(leftValue).getTime() !== new Date(rightValue).getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(Array.isArray(leftValue) &&
|
||||
leftValue.length === 1 &&
|
||||
typeof rightValue === "string" &&
|
||||
!leftValue.includes(rightValue)) ||
|
||||
leftValue !== rightValue
|
||||
);
|
||||
case "contains":
|
||||
return String(leftValue).includes(String(rightValue));
|
||||
case "doesNotContain":
|
||||
return !String(leftValue).includes(String(rightValue));
|
||||
case "startsWith":
|
||||
return String(leftValue).startsWith(String(rightValue));
|
||||
case "doesNotStartWith":
|
||||
return !String(leftValue).startsWith(String(rightValue));
|
||||
case "endsWith":
|
||||
return String(leftValue).endsWith(String(rightValue));
|
||||
case "doesNotEndWith":
|
||||
return !String(leftValue).endsWith(String(rightValue));
|
||||
case "isSubmitted":
|
||||
if (typeof leftValue === "string") {
|
||||
if (
|
||||
condition.leftOperand.type === "question" &&
|
||||
(leftField as TSurveyQuestion).type === TSurveyQuestionTypeEnum.FileUpload &&
|
||||
leftValue
|
||||
) {
|
||||
return leftValue !== "skipped";
|
||||
}
|
||||
return leftValue !== "" && leftValue !== null;
|
||||
} else if (Array.isArray(leftValue)) {
|
||||
return leftValue.length > 0;
|
||||
} else if (typeof leftValue === "number") {
|
||||
return leftValue !== null;
|
||||
}
|
||||
return false;
|
||||
case "isSkipped":
|
||||
return (
|
||||
(Array.isArray(leftValue) && leftValue.length === 0) ||
|
||||
leftValue === "" ||
|
||||
leftValue === null ||
|
||||
leftValue === undefined ||
|
||||
(typeof leftValue === "object" && Object.entries(leftValue).length === 0)
|
||||
);
|
||||
case "isGreaterThan":
|
||||
return Number(leftValue) > Number(rightValue);
|
||||
case "isLessThan":
|
||||
return Number(leftValue) < Number(rightValue);
|
||||
case "isGreaterThanOrEqual":
|
||||
return Number(leftValue) >= Number(rightValue);
|
||||
case "isLessThanOrEqual":
|
||||
return Number(leftValue) <= Number(rightValue);
|
||||
case "equalsOneOf":
|
||||
return Array.isArray(rightValue) && typeof leftValue === "string" && rightValue.includes(leftValue);
|
||||
case "includesAllOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.every((v) => leftValue.includes(v))
|
||||
);
|
||||
case "includesOneOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.some((v) => leftValue.includes(v))
|
||||
);
|
||||
case "doesNotIncludeAllOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.every((v) => !leftValue.includes(v))
|
||||
);
|
||||
case "doesNotIncludeOneOf":
|
||||
return (
|
||||
Array.isArray(leftValue) &&
|
||||
Array.isArray(rightValue) &&
|
||||
rightValue.some((v) => !leftValue.includes(v))
|
||||
);
|
||||
case "isAccepted":
|
||||
return leftValue === "accepted";
|
||||
case "isClicked":
|
||||
return leftValue === "clicked";
|
||||
case "isAfter":
|
||||
return new Date(String(leftValue)) > new Date(String(rightValue));
|
||||
case "isBefore":
|
||||
return new Date(String(leftValue)) < new Date(String(rightValue));
|
||||
case "isBooked":
|
||||
return leftValue === "booked" || !!(leftValue && leftValue !== "");
|
||||
case "isPartiallySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
return Object.values(leftValue).includes("");
|
||||
} else return false;
|
||||
case "isCompletelySubmitted":
|
||||
if (typeof leftValue === "object") {
|
||||
const values = Object.values(leftValue);
|
||||
return values.length > 0 && !values.includes("");
|
||||
} else return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const performCalculation = (
|
||||
survey: TJsEnvironmentStateSurvey,
|
||||
action: TActionCalculate,
|
||||
data: TResponseData,
|
||||
calculations: Record<string, number | string>
|
||||
): number | string | undefined => {
|
||||
const variables = survey.variables || [];
|
||||
const variable = variables.find((v) => v.id === action.variableId);
|
||||
|
||||
if (!variable) return undefined;
|
||||
|
||||
let currentValue = calculations[action.variableId];
|
||||
if (currentValue === undefined) {
|
||||
currentValue = variable.type === "number" ? 0 : "";
|
||||
}
|
||||
let operandValue: string | number | undefined;
|
||||
|
||||
// Determine the operand value based on the action.value type
|
||||
switch (action.value.type) {
|
||||
case "static":
|
||||
operandValue = action.value.value;
|
||||
break;
|
||||
case "variable":
|
||||
const value = calculations[action.value.value];
|
||||
if (typeof value === "number" || typeof value === "string") {
|
||||
operandValue = value;
|
||||
}
|
||||
break;
|
||||
case "question":
|
||||
case "hiddenField":
|
||||
const val = data[action.value.value];
|
||||
if (typeof val === "number" || typeof val === "string") {
|
||||
if (variable.type === "number" && !isNaN(Number(val))) {
|
||||
operandValue = Number(val);
|
||||
}
|
||||
operandValue = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (operandValue === undefined || operandValue === null) return undefined;
|
||||
|
||||
let result: number | string;
|
||||
|
||||
switch (action.operator) {
|
||||
case "add":
|
||||
result = Number(currentValue) + Number(operandValue);
|
||||
break;
|
||||
case "subtract":
|
||||
result = Number(currentValue) - Number(operandValue);
|
||||
break;
|
||||
case "multiply":
|
||||
result = Number(currentValue) * Number(operandValue);
|
||||
break;
|
||||
case "divide":
|
||||
if (Number(operandValue) === 0) return undefined;
|
||||
result = Number(currentValue) / Number(operandValue);
|
||||
break;
|
||||
case "assign":
|
||||
result = operandValue;
|
||||
break;
|
||||
case "concat":
|
||||
result = String(currentValue) + String(operandValue);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,10 +1,38 @@
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { formatDateWithOrdinal, isValidDateString } from "@formbricks/lib/utils/datetime";
|
||||
import { extractFallbackValue, extractId, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { formatDateWithOrdinal, isValidDateString } from "@/lib/date-time";
|
||||
import { getLocalizedValue } from "@/lib/i18n";
|
||||
import { type TResponseData, type TResponseVariables } from "@formbricks/types/responses";
|
||||
import { type TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
// Extracts the ID of recall question from a string containing the "recall" pattern.
|
||||
const extractId = (text: string): string | null => {
|
||||
const pattern = /#recall:([A-Za-z0-9_-]+)/;
|
||||
const match = text.match(pattern);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Extracts the fallback value from a string containing the "fallback" pattern.
|
||||
const extractFallbackValue = (text: string): string => {
|
||||
const pattern = /fallback:(\S*)#/;
|
||||
const match = text.match(pattern);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Extracts the complete recall information (ID and fallback) from a headline string.
|
||||
const extractRecallInfo = (headline: string, id?: string): string | null => {
|
||||
const idPattern = id ? id : "[A-Za-z0-9_-]+";
|
||||
const pattern = new RegExp(`#recall:(${idPattern})\\/fallback:(\\S*)#`);
|
||||
const match = headline.match(pattern);
|
||||
return match ? match[0] : null;
|
||||
};
|
||||
|
||||
export const replaceRecallInfo = (
|
||||
text: string,
|
||||
responseData: TResponseData,
|
||||
@@ -54,7 +82,7 @@ export const parseRecallInformation = (
|
||||
responseData: TResponseData,
|
||||
variables: TResponseVariables
|
||||
) => {
|
||||
const modifiedQuestion = structuredClone(question);
|
||||
const modifiedQuestion = JSON.parse(JSON.stringify(question));
|
||||
if (question.headline[languageCode].includes("recall:")) {
|
||||
modifiedQuestion.headline[languageCode] = replaceRecallInfo(
|
||||
getLocalizedValue(modifiedQuestion.headline, languageCode),
|
||||
|
||||
28
packages/surveys/src/lib/response.ts
Normal file
28
packages/surveys/src/lib/response.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const processResponseData = (
|
||||
responseData: string | number | string[] | Record<string, string>
|
||||
): string => {
|
||||
switch (typeof responseData) {
|
||||
case "string":
|
||||
return responseData;
|
||||
|
||||
case "number":
|
||||
return responseData.toString();
|
||||
|
||||
case "object":
|
||||
if (Array.isArray(responseData)) {
|
||||
responseData = responseData
|
||||
.filter((item) => item !== null && item !== undefined && item !== "")
|
||||
.join(", ");
|
||||
return responseData;
|
||||
} else {
|
||||
const formattedString = Object.entries(responseData)
|
||||
.filter(([_, value]) => value !== "")
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n");
|
||||
return formattedString;
|
||||
}
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
22
packages/surveys/src/lib/storage.ts
Normal file
22
packages/surveys/src/lib/storage.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const getOriginalFileNameFromUrl = (fileURL: string): string => {
|
||||
try {
|
||||
const fileNameFromURL = fileURL.startsWith("/storage/")
|
||||
? fileURL.split("/").pop()
|
||||
: new URL(fileURL).pathname.split("/").pop();
|
||||
|
||||
const fileExt = fileNameFromURL?.split(".").pop() ?? "";
|
||||
const originalFileName = fileNameFromURL?.split("--fid--")[0] ?? "";
|
||||
const fileId = fileNameFromURL?.split("--fid--")[1] ?? "";
|
||||
|
||||
if (!fileId) {
|
||||
const fileName = originalFileName ? decodeURIComponent(originalFileName || "") : "";
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const fileName = originalFileName ? decodeURIComponent(`${originalFileName}.${fileExt}` || "") : "";
|
||||
return fileName;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing file URL: ${error}`);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isLight, mixColor } from "@/lib/color";
|
||||
import global from "@/styles/global.css?inline";
|
||||
import preflight from "@/styles/preflight.css?inline";
|
||||
import calendarCss from "react-calendar/dist/Calendar.css?inline";
|
||||
import datePickerCss from "react-date-picker/dist/DatePicker.css?inline";
|
||||
import { isLight, mixColor } from "@formbricks/lib/utils/colors";
|
||||
import { type TProjectStyling } from "@formbricks/types/project";
|
||||
import { type TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import editorCss from "../../../../apps/web/modules/ui/components/editor/styles-editor-frontend.css?inline";
|
||||
|
||||
36
packages/surveys/src/lib/use-click-outside-hook.ts
Normal file
36
packages/surveys/src/lib/use-click-outside-hook.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { MutableRef, useEffect } from "preact/hooks";
|
||||
|
||||
// Improved version of https://usehooks.com/useOnClickOutside/
|
||||
export const useClickOutside = (
|
||||
ref: MutableRef<HTMLElement | null>,
|
||||
handler: (event: MouseEvent | TouchEvent) => void
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
let startedInside = false;
|
||||
let startedWhenMounted = false;
|
||||
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
// Do nothing if `mousedown` or `touchstart` started inside ref element
|
||||
if (startedInside || !startedWhenMounted) return;
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) return;
|
||||
|
||||
handler(event);
|
||||
};
|
||||
|
||||
const validateEventStart = (event: MouseEvent | TouchEvent) => {
|
||||
startedWhenMounted = ref.current !== null;
|
||||
startedInside = ref.current !== null && ref.current.contains(event.target as Node);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", validateEventStart);
|
||||
document.addEventListener("touchstart", validateEventStart);
|
||||
document.addEventListener("click", listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", validateEventStart);
|
||||
document.removeEventListener("touchstart", validateEventStart);
|
||||
document.removeEventListener("click", listener);
|
||||
};
|
||||
}, [ref, handler]);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ApiResponse, ApiSuccessResponse } from "@/types/api";
|
||||
import { MutableRef, useEffect } from "preact/hooks";
|
||||
import { type Result, err, ok, wrapThrowsAsync } from "@formbricks/types/error-handlers";
|
||||
import { type ApiErrorResponse } from "@formbricks/types/errors";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
@@ -106,39 +105,12 @@ const getPossibleNextQuestions = (question: TSurveyQuestion): string[] => {
|
||||
return possibleDestinations;
|
||||
};
|
||||
|
||||
// Improved version of https://usehooks.com/useOnClickOutside/
|
||||
export const useClickOutside = (
|
||||
ref: MutableRef<HTMLElement | null>,
|
||||
handler: (event: MouseEvent | TouchEvent) => void
|
||||
): void => {
|
||||
useEffect(() => {
|
||||
let startedInside = false;
|
||||
let startedWhenMounted = false;
|
||||
export const isFulfilled = <T>(val: PromiseSettledResult<T>): val is PromiseFulfilledResult<T> => {
|
||||
return val.status === "fulfilled";
|
||||
};
|
||||
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
// Do nothing if `mousedown` or `touchstart` started inside ref element
|
||||
if (startedInside || !startedWhenMounted) return;
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) return;
|
||||
|
||||
handler(event);
|
||||
};
|
||||
|
||||
const validateEventStart = (event: MouseEvent | TouchEvent) => {
|
||||
startedWhenMounted = ref.current !== null;
|
||||
startedInside = ref.current !== null && ref.current.contains(event.target as Node);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", validateEventStart);
|
||||
document.addEventListener("touchstart", validateEventStart);
|
||||
document.addEventListener("click", listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", validateEventStart);
|
||||
document.removeEventListener("touchstart", validateEventStart);
|
||||
document.removeEventListener("click", listener);
|
||||
};
|
||||
}, [ref, handler]);
|
||||
export const isRejected = <T>(val: PromiseSettledResult<T>): val is PromiseRejectedResult => {
|
||||
return val.status === "rejected";
|
||||
};
|
||||
|
||||
export const makeRequest = async <T>(
|
||||
|
||||
127
packages/surveys/src/lib/video-upload.ts
Normal file
127
packages/surveys/src/lib/video-upload.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
export const checkForYoutubeUrl = (url: string): boolean => {
|
||||
try {
|
||||
const youtubeUrl = new URL(url);
|
||||
|
||||
if (youtubeUrl.protocol !== "https:") return false;
|
||||
|
||||
const youtubeDomains = [
|
||||
"www.youtube.com",
|
||||
"www.youtu.be",
|
||||
"www.youtube-nocookie.com",
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
"youtube-nocookie.com",
|
||||
];
|
||||
const hostname = youtubeUrl.hostname;
|
||||
|
||||
return youtubeDomains.includes(hostname);
|
||||
} catch (err) {
|
||||
// invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForVimeoUrl = (url: string): boolean => {
|
||||
try {
|
||||
const vimeoUrl = new URL(url);
|
||||
|
||||
if (vimeoUrl.protocol !== "https:") return false;
|
||||
|
||||
const vimeoDomains = ["www.vimeo.com", "vimeo.com"];
|
||||
const hostname = vimeoUrl.hostname;
|
||||
|
||||
return vimeoDomains.includes(hostname);
|
||||
} catch (err) {
|
||||
// invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForLoomUrl = (url: string): boolean => {
|
||||
try {
|
||||
const loomUrl = new URL(url);
|
||||
|
||||
if (loomUrl.protocol !== "https:") return false;
|
||||
|
||||
const loomDomains = ["www.loom.com", "loom.com"];
|
||||
const hostname = loomUrl.hostname;
|
||||
|
||||
return loomDomains.includes(hostname);
|
||||
} catch (err) {
|
||||
// invalid URL
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const extractYoutubeId = (url: string): string | null => {
|
||||
let id = "";
|
||||
|
||||
// Regular expressions for various YouTube URL formats
|
||||
const regExpList = [
|
||||
/youtu\.be\/([a-zA-Z0-9_-]+)/, // youtu.be/<id>
|
||||
/youtube\.com.*v=([a-zA-Z0-9_-]+)/, // youtube.com/watch?v=<id>
|
||||
/youtube\.com.*embed\/([a-zA-Z0-9_-]+)/, // youtube.com/embed/<id>
|
||||
/youtube-nocookie\.com\/embed\/([a-zA-Z0-9_-]+)/, // youtube-nocookie.com/embed/<id>
|
||||
];
|
||||
|
||||
regExpList.some((regExp) => {
|
||||
const match = url.match(regExp);
|
||||
if (match && match[1]) {
|
||||
id = match[1];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return id || null;
|
||||
};
|
||||
|
||||
const extractVimeoId = (url: string): string | null => {
|
||||
const regExp = /vimeo\.com\/(\d+)/;
|
||||
const match = url.match(regExp);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const extractLoomId = (url: string): string | null => {
|
||||
const regExp = /loom\.com\/share\/([a-zA-Z0-9]+)/;
|
||||
const match = url.match(regExp);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Always convert a given URL into its embed form if supported.
|
||||
export const convertToEmbedUrl = (url: string): string | undefined => {
|
||||
// YouTube
|
||||
if (checkForYoutubeUrl(url)) {
|
||||
const videoId = extractYoutubeId(url);
|
||||
if (videoId) {
|
||||
return `https://www.youtube.com/embed/${videoId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Vimeo
|
||||
if (checkForVimeoUrl(url)) {
|
||||
const videoId = extractVimeoId(url);
|
||||
if (videoId) {
|
||||
return `https://player.vimeo.com/video/${videoId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Loom
|
||||
if (checkForLoomUrl(url)) {
|
||||
const videoId = extractLoomId(url);
|
||||
if (videoId) {
|
||||
return `https://www.loom.com/embed/${videoId}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If no supported platform found, return undefined
|
||||
return undefined;
|
||||
};
|
||||
Reference in New Issue
Block a user