chore: Remove lib dependency from survey package (#4767)

Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
Dhruwang Jariwala
2025-03-04 20:28:00 +05:30
committed by GitHub
parent 40d54d60d4
commit 0e898db710
41 changed files with 889 additions and 155 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
};

View 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}`;
};

View 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 "";
};

View 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;
};

View File

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

View 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 "";
}
};

View 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 "";
}
};

View File

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

View 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]);
};

View File

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

View 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;
};