mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 16:24:08 -06:00
feat: Slick card look (#2531)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
2ca38b1918
commit
3a1683eebd
@@ -13,12 +13,16 @@ export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
|
||||
formbricks.logout();
|
||||
window.location.href = `/${v}`;
|
||||
}}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-[180px] px-4">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="website">Website Surveys</SelectItem>
|
||||
<SelectItem value="app">App Surveys</SelectItem>
|
||||
<SelectItem value="website" className="h-10 px-4 hover:bg-slate-100">
|
||||
Website Surveys
|
||||
</SelectItem>
|
||||
<SelectItem value="app" className="hover:bg-slate-10 h-10 px-4">
|
||||
App Surveys
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
roundness: 8,
|
||||
cardArrangement: {
|
||||
linkSurveys: "simple",
|
||||
inAppSurveys: "simple",
|
||||
appSurveys: "simple",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -132,7 +132,7 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
roundness: 8,
|
||||
cardArrangement: {
|
||||
linkSurveys: "simple",
|
||||
inAppSurveys: "simple",
|
||||
appSurveys: "simple",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
setOpen={setFormStylingOpen}
|
||||
styling={styling}
|
||||
setStyling={setStyling}
|
||||
hideCheckmark
|
||||
isSettingsPage
|
||||
/>
|
||||
|
||||
<CardStylingSettings
|
||||
@@ -199,8 +199,9 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
setOpen={setCardStylingOpen}
|
||||
styling={styling}
|
||||
setStyling={setStyling}
|
||||
hideCheckmark
|
||||
isSettingsPage
|
||||
localProduct={localProduct}
|
||||
surveyType={previewSurveyType}
|
||||
/>
|
||||
|
||||
<BackgroundStylingCard
|
||||
@@ -211,7 +212,7 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
|
||||
environmentId={environmentId}
|
||||
colors={colors}
|
||||
key={styling.background?.bg}
|
||||
hideCheckmark
|
||||
isSettingsPage
|
||||
isUnsplashConfigured={isUnsplashConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -105,9 +105,9 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
|
||||
const highlightBorderColor = product.styling.highlightBorderColor?.light;
|
||||
|
||||
function resetQuestionProgress() {
|
||||
const resetQuestionProgress = () => {
|
||||
setQuestionId(survey?.questions[0]?.id);
|
||||
}
|
||||
};
|
||||
|
||||
const onFileUpload = async (file: File) => file.name;
|
||||
|
||||
@@ -121,7 +121,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-items-center">
|
||||
<div className="flex h-full w-full flex-col items-center justify-items-center overflow-hidden">
|
||||
<motion.div
|
||||
variants={previewParentContainerVariant}
|
||||
className="fixed hidden h-[95%] w-5/6"
|
||||
@@ -158,14 +158,13 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
<Modal
|
||||
isOpen
|
||||
placement={placement}
|
||||
highlightBorderColor={highlightBorderColor}
|
||||
clickOutsideClose={clickOutsideClose}
|
||||
darkOverlay={darkOverlay}
|
||||
previewMode="desktop"
|
||||
background={product.styling.cardBackgroundColor?.light}
|
||||
borderRadius={product.styling.roundness ?? 8}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
survey={{ ...survey, type: "app" }}
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
@@ -187,7 +186,7 @@ export const ThemeStylingPreviewSurvey = ({
|
||||
<div
|
||||
className={`${product.logo?.url && !product.styling.isLogoHidden && !isFullScreenPreview ? "mt-12" : ""} z-0 w-full max-w-md rounded-lg p-4`}>
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface BackgroundStylingCardProps {
|
||||
styling: TSurveyStyling | TProductStyling | null;
|
||||
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | TProductStyling>>;
|
||||
colors: string[];
|
||||
hideCheckmark?: boolean;
|
||||
isSettingsPage?: boolean;
|
||||
disabled?: boolean;
|
||||
environmentId: string;
|
||||
isUnsplashConfigured: boolean;
|
||||
@@ -29,7 +29,7 @@ export default function BackgroundStylingCard({
|
||||
styling,
|
||||
setStyling,
|
||||
colors,
|
||||
hideCheckmark,
|
||||
isSettingsPage = false,
|
||||
disabled,
|
||||
environmentId,
|
||||
isUnsplashConfigured,
|
||||
@@ -79,7 +79,7 @@ export default function BackgroundStylingCard({
|
||||
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
|
||||
)}>
|
||||
<div className="inline-flex px-4 py-4">
|
||||
{!hideCheckmark && (
|
||||
{!isSettingsPage && (
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
@@ -89,10 +89,12 @@ export default function BackgroundStylingCard({
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold text-slate-800">Background Styling</p>
|
||||
{hideCheckmark && <Badge text="Link Surveys" type="gray" size="normal" />}
|
||||
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
|
||||
Background Styling
|
||||
</p>
|
||||
{isSettingsPage && <Badge text="Link Surveys" type="gray" size="normal" />}
|
||||
</div>
|
||||
<p className="mt-1 truncate text-sm text-slate-500">
|
||||
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
|
||||
Change the background to a color, image or animation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,13 @@ import React, { useMemo } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
import { ColorSelectorWithLabel } from "@formbricks/ui/Styling";
|
||||
import { CardArrangement, ColorSelectorWithLabel } from "@formbricks/ui/Styling";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
type CardStylingSettingsProps = {
|
||||
@@ -20,7 +21,7 @@ type CardStylingSettingsProps = {
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
styling: TSurveyStyling | TProductStyling | null;
|
||||
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | TProductStyling>>;
|
||||
hideCheckmark?: boolean;
|
||||
isSettingsPage?: boolean;
|
||||
surveyType?: TSurveyType;
|
||||
disabled?: boolean;
|
||||
localProduct: TProduct;
|
||||
@@ -29,7 +30,7 @@ type CardStylingSettingsProps = {
|
||||
const CardStylingSettings = ({
|
||||
setStyling,
|
||||
styling,
|
||||
hideCheckmark,
|
||||
isSettingsPage = false,
|
||||
surveyType,
|
||||
disabled,
|
||||
open,
|
||||
@@ -43,6 +44,10 @@ const CardStylingSettings = ({
|
||||
|
||||
const isLogoVisible = !!localProduct.logo?.url;
|
||||
|
||||
const linkSurveyCardArrangement = styling?.cardArrangement?.linkSurveys ?? "straight";
|
||||
|
||||
const inAppSurveyCardArrangement = styling?.cardArrangement?.appSurveys ?? "straight";
|
||||
|
||||
const setCardBgColor = (color: string) => {
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
@@ -113,6 +118,24 @@ const CardStylingSettings = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const setCardArrangement = (arrangement: TCardArrangementOptions, surveyType: TSurveyType) => {
|
||||
const newCardArrangement = {
|
||||
linkSurveys: linkSurveyCardArrangement,
|
||||
appSurveys: inAppSurveyCardArrangement,
|
||||
};
|
||||
|
||||
if (surveyType === "link") {
|
||||
newCardArrangement.linkSurveys = arrangement;
|
||||
} else if (surveyType === "app" || surveyType === "website") {
|
||||
newCardArrangement.appSurveys = arrangement;
|
||||
}
|
||||
|
||||
setStyling((prev) => ({
|
||||
...prev,
|
||||
cardArrangement: newCardArrangement,
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleProgressBarVisibility = (hideProgressBar: boolean) => {
|
||||
setStyling({
|
||||
...styling,
|
||||
@@ -147,7 +170,7 @@ const CardStylingSettings = ({
|
||||
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
|
||||
)}>
|
||||
<div className="inline-flex px-4 py-4">
|
||||
{!hideCheckmark && (
|
||||
{!isSettingsPage && (
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
@@ -157,8 +180,12 @@ const CardStylingSettings = ({
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">Card Styling</p>
|
||||
<p className="mt-1 text-sm text-slate-500">Style the survey card.</p>
|
||||
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
|
||||
Card Styling
|
||||
</p>
|
||||
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
|
||||
Style the survey card.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
@@ -198,6 +225,12 @@ const CardStylingSettings = ({
|
||||
description="Change the shadow color of the card."
|
||||
/>
|
||||
|
||||
<CardArrangement
|
||||
surveyType={isAppSurvey ? "app" : "link"}
|
||||
activeCardArrangement={isAppSurvey ? inAppSurveyCardArrangement : linkSurveyCardArrangement}
|
||||
setActiveCardArrangement={setCardArrangement}
|
||||
/>
|
||||
|
||||
<>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch
|
||||
@@ -215,14 +248,14 @@ const CardStylingSettings = ({
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{isLogoVisible && (!surveyType || surveyType === "link") && (
|
||||
{isLogoVisible && (!surveyType || surveyType === "link") && !isSettingsPage && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch id="isLogoHidden" checked={isLogoHidden} onCheckedChange={toggleLogoVisibility} />
|
||||
<Label htmlFor="isLogoHidden" className="cursor-pointer">
|
||||
<div className="ml-2 flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide logo</h3>
|
||||
{hideCheckmark && <Badge text="Link Surveys" type="gray" size="normal" />}
|
||||
<Badge text="Link Surveys" type="gray" size="normal" />
|
||||
</div>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Hides the logo in this specific survey
|
||||
@@ -238,8 +271,15 @@ const CardStylingSettings = ({
|
||||
<Switch checked={isHighlightBorderAllowed} onCheckedChange={setIsHighlightBorderAllowed} />
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Add highlight border</h3>
|
||||
<Badge text="In-App and Website Surveys" type="gray" size="normal" />
|
||||
<h3 className="whitespace-nowrap text-sm font-semibold text-slate-700">
|
||||
Add highlight border
|
||||
</h3>
|
||||
<Badge
|
||||
text="App & Website Surveys"
|
||||
type="gray"
|
||||
size="normal"
|
||||
className="whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Add an outer border to your survey card.</p>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ type FormStylingSettingsProps = {
|
||||
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | TProductStyling>>;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
hideCheckmark?: boolean;
|
||||
isSettingsPage?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ const FormStylingSettings = ({
|
||||
styling,
|
||||
setStyling,
|
||||
open,
|
||||
hideCheckmark = false,
|
||||
isSettingsPage = false,
|
||||
disabled = false,
|
||||
setOpen,
|
||||
}: FormStylingSettingsProps) => {
|
||||
@@ -134,7 +134,7 @@ const FormStylingSettings = ({
|
||||
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
|
||||
)}>
|
||||
<div className="inline-flex px-4 py-4">
|
||||
{!hideCheckmark && (
|
||||
{!isSettingsPage && (
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
@@ -144,8 +144,10 @@ const FormStylingSettings = ({
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">Form Styling</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
|
||||
Form Styling
|
||||
</p>
|
||||
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
|
||||
Style the question texts, descriptions and input fields.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getPlacementStyle } from "@/app/lib/preview";
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
@@ -9,7 +9,6 @@ export default function Modal({
|
||||
isOpen,
|
||||
placement,
|
||||
previewMode,
|
||||
highlightBorderColor,
|
||||
clickOutsideClose,
|
||||
darkOverlay,
|
||||
borderRadius,
|
||||
@@ -19,7 +18,6 @@ export default function Modal({
|
||||
isOpen: boolean;
|
||||
placement: TPlacement;
|
||||
previewMode: string;
|
||||
highlightBorderColor: string | null | undefined;
|
||||
clickOutsideClose: boolean;
|
||||
darkOverlay: boolean;
|
||||
borderRadius?: number;
|
||||
@@ -102,14 +100,6 @@ export default function Modal({
|
||||
};
|
||||
}, [clickOutsideClose, scalingClasses.transformOrigin]);
|
||||
|
||||
const highlightBorderColorStyle = useMemo(() => {
|
||||
if (!highlightBorderColor) return;
|
||||
|
||||
return {
|
||||
border: `2px solid ${highlightBorderColor}`,
|
||||
};
|
||||
}, [highlightBorderColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setShow(isOpen);
|
||||
}, [isOpen]);
|
||||
@@ -144,7 +134,6 @@ export default function Modal({
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{
|
||||
...highlightBorderColorStyle,
|
||||
...scalingClasses,
|
||||
...(borderRadius && {
|
||||
borderRadius: `${borderRadius}px`,
|
||||
@@ -154,7 +143,7 @@ export default function Modal({
|
||||
}),
|
||||
}}
|
||||
className={cn(
|
||||
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm bg-white shadow-lg transition-all duration-500 ease-in-out ",
|
||||
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm transition-all duration-500 ease-in-out ",
|
||||
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full",
|
||||
slidingAnimationClass
|
||||
)}>
|
||||
|
||||
@@ -195,6 +195,7 @@ export const PreviewSurvey = ({
|
||||
const handlePreviewModalClose = () => {
|
||||
setIsModalOpen(false);
|
||||
setTimeout(() => {
|
||||
setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
setIsModalOpen(true);
|
||||
}, 1000);
|
||||
};
|
||||
@@ -238,7 +239,6 @@ export const PreviewSurvey = ({
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={placement}
|
||||
highlightBorderColor={styling.highlightBorderColor?.light}
|
||||
previewMode="mobile"
|
||||
darkOverlay={darkOverlay}
|
||||
clickOutsideClose={clickOutsideClose}
|
||||
@@ -266,9 +266,9 @@ export const PreviewSurvey = ({
|
||||
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
|
||||
)}
|
||||
</div>
|
||||
<div className="no-scrollbar z-10 w-full border border-transparent">
|
||||
<div className=" z-10 w-full max-w-md rounded-lg border border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
@@ -324,7 +324,6 @@ export const PreviewSurvey = ({
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={placement}
|
||||
highlightBorderColor={styling.highlightBorderColor?.light}
|
||||
clickOutsideClose={clickOutsideClose}
|
||||
darkOverlay={darkOverlay}
|
||||
previewMode="desktop"
|
||||
@@ -354,7 +353,7 @@ export const PreviewSurvey = ({
|
||||
</div>
|
||||
<div className="z-0 w-full max-w-md rounded-lg border-transparent">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
survey={{ ...survey, type: "link" }}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { StackedCardsContainer } from "@formbricks/ui/StackedCardsContainer";
|
||||
|
||||
export default function InvalidLanguage() {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center text-center">
|
||||
<StackedCardsContainer>
|
||||
<span className="h-24 w-24 rounded-full bg-slate-200 p-6 text-5xl">🈂️</span>
|
||||
<p className="mt-8 text-4xl font-bold">Survey not available in specified language</p>
|
||||
</StackedCardsContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
||||
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
|
||||
import { VerifyEmail } from "@/app/s/[surveyId]/components/VerifyEmail";
|
||||
import { getPrefillValue } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -154,10 +154,24 @@ export default function LinkSurvey({
|
||||
|
||||
if (survey.verifyEmail && emailVerificationStatus !== "verified") {
|
||||
if (emailVerificationStatus === "fishy") {
|
||||
return <VerifyEmail survey={survey} isErrorComponent={true} languageCode={languageCode} />;
|
||||
return (
|
||||
<VerifyEmail
|
||||
survey={survey}
|
||||
isErrorComponent={true}
|
||||
languageCode={languageCode}
|
||||
styling={product.styling}
|
||||
/>
|
||||
);
|
||||
}
|
||||
//emailVerificationStatus === "not-verified"
|
||||
return <VerifyEmail singleUseId={suId ?? ""} survey={survey} languageCode={languageCode} />;
|
||||
return (
|
||||
<VerifyEmail
|
||||
singleUseId={suId ?? ""}
|
||||
survey={survey}
|
||||
languageCode={languageCode}
|
||||
styling={product.styling}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const determineStyling = () => {
|
||||
@@ -193,7 +207,6 @@ export default function LinkSurvey({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
styling={determineStyling()}
|
||||
|
||||
@@ -166,7 +166,9 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
};
|
||||
|
||||
const renderContent = () => (
|
||||
<div className="no-scrollbar absolute flex h-full w-full items-center justify-center">{children}</div>
|
||||
<div className="no-scrollbar absolute flex h-full w-full items-center justify-center overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobilePreview) {
|
||||
@@ -182,7 +184,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
|
||||
);
|
||||
} else if (isEditorView) {
|
||||
return (
|
||||
<div ref={ContentRef} className="flex flex-grow flex-col rounded-b-lg">
|
||||
<div ref={ContentRef} className="overflow-hiddem flex flex-grow flex-col rounded-b-lg">
|
||||
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
|
||||
{renderBackground()}
|
||||
<div className="flex h-full w-full items-center justify-center">{children}</div>
|
||||
|
||||
@@ -8,22 +8,27 @@ import { Toaster, toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { StackedCardsContainer } from "@formbricks/ui/StackedCardsContainer";
|
||||
|
||||
export default function VerifyEmail({
|
||||
survey,
|
||||
isErrorComponent,
|
||||
singleUseId,
|
||||
languageCode,
|
||||
}: {
|
||||
interface VerifyEmailProps {
|
||||
survey: TSurvey;
|
||||
isErrorComponent?: boolean;
|
||||
singleUseId?: string;
|
||||
languageCode: string;
|
||||
}) {
|
||||
styling: TProductStyling;
|
||||
}
|
||||
|
||||
export const VerifyEmail = ({
|
||||
survey,
|
||||
isErrorComponent,
|
||||
singleUseId,
|
||||
languageCode,
|
||||
styling,
|
||||
}: VerifyEmailProps) => {
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
}, [survey]);
|
||||
@@ -87,7 +92,10 @@ export default function VerifyEmail({
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center text-center">
|
||||
<Toaster />
|
||||
<StackedCardsContainer>
|
||||
<StackedCardsContainer
|
||||
cardArrangement={
|
||||
survey.styling?.cardArrangement?.linkSurveys ?? styling.cardArrangement?.linkSurveys ?? "straight"
|
||||
}>
|
||||
{!emailSent && !showPreviewQuestions && (
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto rounded-full border bg-slate-200 p-6">
|
||||
@@ -132,7 +140,6 @@ export default function VerifyEmail({
|
||||
)}
|
||||
{emailSent && (
|
||||
<div>
|
||||
{" "}
|
||||
<h1 className="mt-8 text-2xl font-bold lg:text-4xl">Check your email.</h1>
|
||||
<p className="mt-4 text-center text-sm text-slate-400 lg:text-base">
|
||||
We sent an email to <span className="font-semibold italic">{email}</span>. Please click the link
|
||||
@@ -146,4 +153,4 @@ export default function VerifyEmail({
|
||||
</StackedCardsContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +65,9 @@ test.describe("JS Package Test", async () => {
|
||||
// expect(displayApi.status()).toBe(200);
|
||||
|
||||
// Formbricks Modal is visible
|
||||
await expect(page.getByRole("link", { name: "Powered by Formbricks" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-0").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(1500);
|
||||
@@ -87,24 +89,31 @@ test.describe("JS Package Test", async () => {
|
||||
await expect(page.locator("#formbricks-modal-container")).toHaveCount(1);
|
||||
|
||||
// Formbricks Modal is visible
|
||||
await expect(page.getByRole("link", { name: "Powered by Formbricks" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-0").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
|
||||
// Fill the Survey
|
||||
await page.getByRole("button", { name: "Happy to help!" }).click();
|
||||
await page.locator("label").filter({ hasText: "Somewhat disappointed" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-1").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("label").filter({ hasText: "Founder" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByLabel("").fill("People who believe that PMF is necessary");
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByLabel("").fill("Much higher response rates!");
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByLabel("How can we improve My Product").fill("Make this end to end test pass!");
|
||||
await page.locator("#questionCard-2").getByRole("button", { name: "Next" }).click();
|
||||
await page
|
||||
.locator("#questionCard-3")
|
||||
.getByLabel("textarea")
|
||||
.fill("People who believe that PMF is necessary");
|
||||
await page.locator("#questionCard-3").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-4").getByLabel("textarea").fill("Much higher response rates!");
|
||||
await page.locator("#questionCard-4").getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-5").getByLabel("textarea").fill("Make this end to end test pass!");
|
||||
await page.getByRole("button", { name: "Finish" }).click();
|
||||
await page.getByText("Thank you!").click();
|
||||
|
||||
// Formbricks Modal is not visible
|
||||
await expect(page.getByText("Powered by Formbricks")).not.toBeVisible({ timeout: 10000 });
|
||||
await expect(
|
||||
page.locator("#questionCard-6").getByRole("link", { name: "Powered by Formbricks" })
|
||||
).toBeVisible();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(5000);
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ test.describe("Survey Create & Submit Response", async () => {
|
||||
// Welcome Card
|
||||
await expect(page.getByText(surveys.createAndSubmit.welcomeCard.headline)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.welcomeCard.description)).toBeVisible();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard--1").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Open Text Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.openTextQuestion.question)).toBeVisible();
|
||||
@@ -79,53 +79,76 @@ test.describe("Survey Create & Submit Response", async () => {
|
||||
await page
|
||||
.getByPlaceholder(surveys.createAndSubmit.openTextQuestion.placeholder)
|
||||
.fill("This is my Open Text answer");
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-0").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Single Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.description)).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.singleSelectQuestion.options.length; i++) {
|
||||
await expect(page.getByText(surveys.createAndSubmit.singleSelectQuestion.options[i])).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.locator("#questionCard-1 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.singleSelectQuestion.options[i] })
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(page.getByText("Other")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByText(surveys.createAndSubmit.singleSelectQuestion.options[0]).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(page.locator("#questionCard-1").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-1").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page
|
||||
.locator("#questionCard-1 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.singleSelectQuestion.options[0] })
|
||||
.click();
|
||||
await page.locator("#questionCard-1").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Multi Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.description)).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.multiSelectQuestion.options.length; i++) {
|
||||
await expect(page.getByText(surveys.createAndSubmit.multiSelectQuestion.options[i])).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.singleSelectQuestion.options.length; i++) {
|
||||
await expect(
|
||||
page
|
||||
.locator("#questionCard-2 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.multiSelectQuestion.options[i] })
|
||||
).toBeVisible();
|
||||
}
|
||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByText(surveys.createAndSubmit.multiSelectQuestion.options[0]).click();
|
||||
await page.getByText(surveys.createAndSubmit.multiSelectQuestion.options[1]).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await expect(page.locator("#questionCard-2").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-2").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
for (let i = 0; i < surveys.createAndSubmit.multiSelectQuestion.options.length; i++) {
|
||||
await page
|
||||
.locator("#questionCard-2 label")
|
||||
.filter({ hasText: surveys.createAndSubmit.multiSelectQuestion.options[i] })
|
||||
.click();
|
||||
}
|
||||
await page.locator("#questionCard-2").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Rating Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.description)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.lowLabel)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.ratingQuestion.highLabel)).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.lowLabel)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-3").getByText(surveys.createAndSubmit.ratingQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
expect(await page.getByRole("group", { name: "Choices" }).locator("label").count()).toBe(5);
|
||||
await expect(page.getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-3").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.locator("path").nth(3).click();
|
||||
|
||||
// NPS Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.npsQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.npsQuestion.lowLabel)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.npsQuestion.highLabel)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.lowLabel)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator("#questionCard-4").getByText(surveys.createAndSubmit.npsQuestion.highLabel)
|
||||
).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
|
||||
for (let i = 0; i < 11; i++) {
|
||||
await expect(page.getByText(`${i}`, { exact: true })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-4").getByText(`${i}`, { exact: true })).toBeVisible();
|
||||
}
|
||||
await page.getByText("8").click();
|
||||
await page.locator("#questionCard-4").getByText("8", { exact: true }).click();
|
||||
|
||||
// CTA Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.ctaQuestion.question)).toBeVisible();
|
||||
@@ -137,25 +160,25 @@ test.describe("Survey Create & Submit Response", async () => {
|
||||
// Consent Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-6").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByText(surveys.createAndSubmit.consentQuestion.checkboxLabel).check();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-6").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Picture Select Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.pictureSelectQuestion.question)).toBeVisible();
|
||||
await expect(page.getByText(surveys.createAndSubmit.pictureSelectQuestion.description)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-7").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "puppy-1-small.jpg" })).toBeVisible();
|
||||
await expect(page.getByRole("img", { name: "puppy-2-small.jpg" })).toBeVisible();
|
||||
await page.getByRole("img", { name: "puppy-1-small.jpg" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-7").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// File Upload Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.fileUploadQuestion.question)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-8").getByRole("button", { name: "Next" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-8").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator("label").filter({ hasText: "Click or drag to upload files." }).locator("div").nth(0)
|
||||
).toBeVisible();
|
||||
@@ -165,7 +188,7 @@ test.describe("Survey Create & Submit Response", async () => {
|
||||
buffer: Buffer.from("this is test"),
|
||||
});
|
||||
await page.getByText("Uploading...").waitFor({ state: "hidden" });
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-8").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Matrix Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.matrix.question)).toBeVisible();
|
||||
@@ -177,9 +200,10 @@ test.describe("Survey Create & Submit Response", async () => {
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[1] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[2] })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: surveys.createAndSubmit.matrix.columns[3] })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Next" })).not.toBeVisible();
|
||||
await expect(page.locator("#questionCard-9").getByRole("button", { name: "Back" })).toBeVisible();
|
||||
await page.getByRole("row", { name: "Rose 🌹" }).getByRole("cell").nth(1).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.locator("#questionCard-9").getByRole("button", { name: "Next" }).click();
|
||||
|
||||
// Address Question
|
||||
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
|
||||
|
||||
@@ -30,6 +30,7 @@ interface QuestionConditionalProps {
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
surveyId: string;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const QuestionConditional = ({
|
||||
@@ -46,6 +47,7 @@ export const QuestionConditional = ({
|
||||
surveyId,
|
||||
onFileUpload,
|
||||
isInIframe,
|
||||
currentQuestionId,
|
||||
}: QuestionConditionalProps) => {
|
||||
return question.type === TSurveyQuestionType.OpenText ? (
|
||||
<OpenTextQuestion
|
||||
@@ -61,6 +63,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||
<MultipleChoiceSingleQuestion
|
||||
@@ -76,6 +79,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||
<MultipleChoiceMultiQuestion
|
||||
@@ -91,6 +95,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.NPS ? (
|
||||
<NPSQuestion
|
||||
@@ -106,6 +111,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.CTA ? (
|
||||
<CTAQuestion
|
||||
@@ -121,6 +127,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Rating ? (
|
||||
<RatingQuestion
|
||||
@@ -136,6 +143,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Consent ? (
|
||||
<ConsentQuestion
|
||||
@@ -151,6 +159,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
<DateQuestion
|
||||
@@ -166,6 +175,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||
<PictureSelectionQuestion
|
||||
@@ -181,6 +191,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
||||
<FileUploadQuestion
|
||||
@@ -198,6 +209,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<CalQuestion
|
||||
@@ -213,6 +225,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
isInIframe={isInIframe}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Matrix ? (
|
||||
<MatrixQuestion
|
||||
@@ -226,6 +239,7 @@ export const QuestionConditional = ({
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Address ? (
|
||||
<AddressQuestion
|
||||
@@ -240,6 +254,7 @@ export const QuestionConditional = ({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={currentQuestionId}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -2,9 +2,11 @@ import { FormbricksBranding } from "@/components/general/FormbricksBranding";
|
||||
import { ProgressBar } from "@/components/general/ProgressBar";
|
||||
import { QuestionConditional } from "@/components/general/QuestionConditional";
|
||||
import { ResponseErrorComponent } from "@/components/general/ResponseErrorComponent";
|
||||
import { SurveyCloseButton } from "@/components/general/SurveyCloseButton";
|
||||
import { ThankYouCard } from "@/components/general/ThankYouCard";
|
||||
import { WelcomeCard } from "@/components/general/WelcomeCard";
|
||||
import { AutoCloseWrapper } from "@/components/wrappers/AutoCloseWrapper";
|
||||
import { StackedCardsContainer } from "@/components/wrappers/StackedCardsContainer";
|
||||
import { evaluateCondition } from "@/lib/logicEvaluator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
@@ -34,7 +36,6 @@ export const Survey = ({
|
||||
getSetQuestionId,
|
||||
onFileUpload,
|
||||
responseCount,
|
||||
isCardBorderVisible = true,
|
||||
startAtQuestionId,
|
||||
}: SurveyBaseProps) => {
|
||||
const isInIframe = window.self !== window.top;
|
||||
@@ -51,6 +52,14 @@ export const Survey = ({
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [responseData, setResponseData] = useState<TResponseData>({});
|
||||
const [ttc, setTtc] = useState<TResponseTtc>({});
|
||||
const cardArrangement = useMemo(() => {
|
||||
if (survey.type === "link") {
|
||||
return styling.cardArrangement?.linkSurveys ?? "straight";
|
||||
} else {
|
||||
return styling.cardArrangement?.appSurveys ?? "straight";
|
||||
}
|
||||
}, [survey.type, styling.cardArrangement?.linkSurveys, styling.cardArrangement?.appSurveys]);
|
||||
|
||||
const currentQuestionIndex = survey.questions.findIndex((q) => q.id === questionId);
|
||||
const currentQuestion = useMemo(() => {
|
||||
if (questionId === "end" && !survey.thankYouCard.enabled) {
|
||||
@@ -249,82 +258,83 @@ export const Survey = ({
|
||||
setQuestionId(prevQuestionId);
|
||||
};
|
||||
|
||||
const getCardContent = (): JSX.Element | undefined => {
|
||||
const getCardContent = (questionIdx: number, offset: number): JSX.Element | undefined => {
|
||||
if (showError) {
|
||||
return (
|
||||
<ResponseErrorComponent responseData={responseData} questions={survey.questions} onRetry={onRetry} />
|
||||
);
|
||||
}
|
||||
if (questionId === "start" && survey.welcomeCard.enabled) {
|
||||
return (
|
||||
<WelcomeCard
|
||||
headline={survey.welcomeCard.headline}
|
||||
html={survey.welcomeCard.html}
|
||||
fileUrl={survey.welcomeCard.fileUrl}
|
||||
buttonLabel={survey.welcomeCard.buttonLabel}
|
||||
onSubmit={onSubmit}
|
||||
survey={survey}
|
||||
languageCode={languageCode}
|
||||
responseCount={responseCount}
|
||||
isInIframe={isInIframe}
|
||||
/>
|
||||
);
|
||||
} else if (questionId === "end" && survey.thankYouCard.enabled) {
|
||||
return (
|
||||
<ThankYouCard
|
||||
headline={survey.thankYouCard.headline}
|
||||
subheader={survey.thankYouCard.subheader}
|
||||
isResponseSendingFinished={isResponseSendingFinished}
|
||||
buttonLabel={survey.thankYouCard.buttonLabel}
|
||||
buttonLink={survey.thankYouCard.buttonLink}
|
||||
imageUrl={survey.thankYouCard.imageUrl}
|
||||
videoUrl={survey.thankYouCard.videoUrl}
|
||||
redirectUrl={survey.redirectUrl}
|
||||
isRedirectDisabled={isRedirectDisabled}
|
||||
languageCode={languageCode}
|
||||
replaceRecallInfo={replaceRecallInfo}
|
||||
isInIframe={isInIframe}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
currentQuestion && (
|
||||
<QuestionConditional
|
||||
surveyId={survey.id}
|
||||
question={parseRecallInformation(currentQuestion)}
|
||||
value={responseData[currentQuestion.id]}
|
||||
onChange={onChange}
|
||||
const content = () => {
|
||||
if (questionIdx === -1) {
|
||||
return (
|
||||
<WelcomeCard
|
||||
headline={survey.welcomeCard.headline}
|
||||
html={survey.welcomeCard.html}
|
||||
fileUrl={survey.welcomeCard.fileUrl}
|
||||
buttonLabel={survey.welcomeCard.buttonLabel}
|
||||
onSubmit={onSubmit}
|
||||
onBack={onBack}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
onFileUpload={onFileUpload}
|
||||
isFirstQuestion={currentQuestion.id === survey?.questions[0]?.id}
|
||||
isLastQuestion={currentQuestion.id === survey.questions[survey.questions.length - 1].id}
|
||||
survey={survey}
|
||||
languageCode={languageCode}
|
||||
responseCount={responseCount}
|
||||
isInIframe={isInIframe}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
);
|
||||
} else if (questionIdx === survey.questions.length) {
|
||||
return (
|
||||
<ThankYouCard
|
||||
headline={survey.thankYouCard.headline}
|
||||
subheader={survey.thankYouCard.subheader}
|
||||
isResponseSendingFinished={isResponseSendingFinished}
|
||||
buttonLabel={survey.thankYouCard.buttonLabel}
|
||||
buttonLink={survey.thankYouCard.buttonLink}
|
||||
imageUrl={survey.thankYouCard.imageUrl}
|
||||
videoUrl={survey.thankYouCard.videoUrl}
|
||||
redirectUrl={survey.redirectUrl}
|
||||
isRedirectDisabled={isRedirectDisabled}
|
||||
languageCode={languageCode}
|
||||
replaceRecallInfo={replaceRecallInfo}
|
||||
isInIframe={isInIframe}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const question = survey.questions[questionIdx];
|
||||
return (
|
||||
question && (
|
||||
<QuestionConditional
|
||||
surveyId={survey.id}
|
||||
question={parseRecallInformation(question)}
|
||||
value={responseData[question.id]}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
onBack={onBack}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
onFileUpload={onFileUpload}
|
||||
isFirstQuestion={
|
||||
history && prefillResponseData
|
||||
? history[history.length - 1] === survey.questions[0].id
|
||||
: question.id === survey?.questions[0]?.id
|
||||
}
|
||||
isLastQuestion={question.id === survey.questions[survey.questions.length - 1].id}
|
||||
languageCode={languageCode}
|
||||
isInIframe={isInIframe}
|
||||
currentQuestionId={questionId}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
||||
{offset === 0 && survey.type !== "link" && <SurveyCloseButton onClose={onClose} />}
|
||||
<div
|
||||
className={cn(
|
||||
"no-scrollbar md:rounded-custom bg-survey-bg rounded-t-custom flex h-full w-full flex-col justify-between",
|
||||
isCardBorderVisible ? "border-survey-border border" : "",
|
||||
survey.type === "link" ? "fb-survey-shadow" : ""
|
||||
"no-scrollbar md:rounded-custom rounded-t-custom bg-survey-bg flex h-full w-full flex-col justify-between overflow-hidden transition-all duration-1000 ease-in-out",
|
||||
survey.type === "link" ? "fb-survey-shadow" : "",
|
||||
offset === 0 || cardArrangement === "simple" ? "opacity-100" : "opacity-0"
|
||||
)}>
|
||||
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
|
||||
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
|
||||
// Handle the case when there are no questions and both welcome and thank you cards are disabled
|
||||
<div>No questions available.</div>
|
||||
) : (
|
||||
getCardContent()
|
||||
)}
|
||||
{content()}
|
||||
</div>
|
||||
<div className="mx-6 mb-10 mt-2 space-y-3 md:mb-6 md:mt-6">
|
||||
{isBrandingEnabled && <FormbricksBranding />}
|
||||
@@ -332,6 +342,17 @@ export const Survey = ({
|
||||
</div>
|
||||
</div>
|
||||
</AutoCloseWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StackedCardsContainer
|
||||
cardArrangement={cardArrangement}
|
||||
currentQuestionId={questionId}
|
||||
getCardContent={getCardContent}
|
||||
survey={survey}
|
||||
styling={styling}
|
||||
setQuestionId={setQuestionId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
interface SurveyCloseButtonProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SurveyCloseButton = ({ onClose }: SurveyCloseButtonProps) => {
|
||||
return (
|
||||
<div class="absolute right-0 top-0 z-[1001] block pr-1 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
class="text-close-button hover:text-close-button-focus focus:ring-close-button-focus relative h-4 w-4 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
<svg
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4L20 20M4 20L20 4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export function SurveyInline(props: SurveyInlineProps) {
|
||||
<div id="fbjs" className="formbricks-form h-full w-full">
|
||||
{isMobile ? (
|
||||
<div className="flex h-screen w-full flex-col justify-end overflow-hidden">
|
||||
<div className="overflow-auto pt-[11vh]">
|
||||
<div>
|
||||
<Survey {...props} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ export const WelcomeCard = ({
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
<div className="mx-6 mt-4 flex gap-4">
|
||||
<div className="mx-6 mt-4 flex gap-4 py-4">
|
||||
<SubmitButton
|
||||
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
|
||||
isLastQuestion={false}
|
||||
|
||||
@@ -24,6 +24,7 @@ interface AddressQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const AddressQuestion = ({
|
||||
@@ -38,12 +39,13 @@ export const AddressQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
isInIframe,
|
||||
currentQuestionId,
|
||||
}: AddressQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hasFilled, setHasFilled] = useState(false);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const safeValue = useMemo(() => {
|
||||
return Array.isArray(value) ? value : ["", "", "", "", "", ""];
|
||||
|
||||
@@ -24,6 +24,7 @@ interface CTAQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const CTAQuestion = ({
|
||||
@@ -37,11 +38,12 @@ export const CTAQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
isInIframe,
|
||||
currentQuestionId,
|
||||
}: CTAQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
return (
|
||||
<div key={question.id}>
|
||||
|
||||
@@ -25,6 +25,7 @@ interface CalQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const CalQuestion = ({
|
||||
@@ -38,11 +39,12 @@ export const CalQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: CalQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const onSuccessfulBooking = useCallback(() => {
|
||||
onChange({ [question.id]: "booked" });
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ConsentQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const ConsentQuestion = ({
|
||||
@@ -36,11 +37,12 @@ export const ConsentQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: ConsentQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
||||
@@ -29,6 +29,7 @@ interface DateQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
const CalendarIcon = () => (
|
||||
@@ -87,12 +88,12 @@ export const DateQuestion = ({
|
||||
languageCode,
|
||||
setTtc,
|
||||
ttc,
|
||||
currentQuestionId,
|
||||
}: DateQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const [datePickerOpen, setDatePickerOpen] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(value ? new Date(value) : undefined);
|
||||
@@ -106,7 +107,7 @@ export const DateQuestion = ({
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
}, [datePickerOpen]);
|
||||
}, [datePickerOpen, selectedDate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!selectedDate) {
|
||||
|
||||
@@ -28,6 +28,7 @@ interface FileUploadQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const FileUploadQuestion = ({
|
||||
@@ -43,11 +44,11 @@ export const FileUploadQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: FileUploadQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
||||
@@ -23,6 +23,7 @@ interface MatrixQuestionProps {
|
||||
languageCode: string;
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const MatrixQuestion = ({
|
||||
@@ -36,11 +37,12 @@ export const MatrixQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: MatrixQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
const isSubmitButtonVisible = question.required ? Object.entries(value).length !== 0 : true;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(column: string, row: string) => {
|
||||
|
||||
@@ -24,6 +24,7 @@ interface MultipleChoiceMultiProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const MultipleChoiceMultiQuestion = ({
|
||||
@@ -38,11 +39,11 @@ export const MultipleChoiceMultiQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
isInIframe,
|
||||
currentQuestionId,
|
||||
}: MultipleChoiceMultiProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const getChoicesWithoutOtherLabels = useCallback(
|
||||
() =>
|
||||
|
||||
@@ -24,6 +24,7 @@ interface MultipleChoiceSingleProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const MultipleChoiceSingleQuestion = ({
|
||||
@@ -38,6 +39,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
isInIframe,
|
||||
currentQuestionId,
|
||||
}: MultipleChoiceSingleProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [otherSelected, setOtherSelected] = useState(false);
|
||||
@@ -45,7 +47,7 @@ export const MultipleChoiceSingleQuestion = ({
|
||||
const choicesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const questionChoices = useMemo(() => {
|
||||
if (!question.choices) {
|
||||
|
||||
@@ -24,6 +24,7 @@ interface NPSQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const NPSQuestion = ({
|
||||
@@ -37,12 +38,13 @@ export const NPSQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: NPSQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
||||
@@ -26,6 +26,7 @@ interface OpenTextQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const OpenTextQuestion = ({
|
||||
@@ -40,10 +41,12 @@ export const OpenTextQuestion = ({
|
||||
ttc,
|
||||
setTtc,
|
||||
isInIframe,
|
||||
currentQuestionId,
|
||||
}: OpenTextQuestionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const handleInputChange = (inputValue: string) => {
|
||||
onChange({ [question.id]: inputValue });
|
||||
@@ -113,6 +116,7 @@ export const OpenTextQuestion = ({
|
||||
rows={3}
|
||||
name={question.id}
|
||||
tabIndex={1}
|
||||
aria-label="textarea"
|
||||
id={question.id}
|
||||
placeholder={getLocalizedValue(question.placeholder, languageCode)}
|
||||
required={question.required}
|
||||
|
||||
@@ -24,6 +24,7 @@ interface PictureSelectionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const PictureSelectionQuestion = ({
|
||||
@@ -37,11 +38,12 @@ export const PictureSelectionQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: PictureSelectionProps) => {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const addItem = (item: string) => {
|
||||
let values: string[] = [];
|
||||
|
||||
@@ -37,6 +37,7 @@ interface RatingQuestionProps {
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
isInIframe: boolean;
|
||||
currentQuestionId: string;
|
||||
}
|
||||
|
||||
export const RatingQuestion = ({
|
||||
@@ -50,12 +51,13 @@ export const RatingQuestion = ({
|
||||
languageCode,
|
||||
ttc,
|
||||
setTtc,
|
||||
currentQuestionId,
|
||||
}: RatingQuestionProps) => {
|
||||
const [hoveredNumber, setHoveredNumber] = useState(0);
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const handleSelect = (number: number) => {
|
||||
onChange({ [question.id]: number });
|
||||
@@ -76,7 +78,7 @@ export const RatingQuestion = ({
|
||||
name="rating"
|
||||
value={number}
|
||||
className="invisible absolute left-0 h-full w-full cursor-pointer opacity-0"
|
||||
onChange={() => handleSelect(number)}
|
||||
onClick={() => handleSelect(number)}
|
||||
required={question.required}
|
||||
checked={value === number}
|
||||
/>
|
||||
@@ -224,7 +226,7 @@ export const RatingQuestion = ({
|
||||
/>
|
||||
)}
|
||||
<div></div>
|
||||
{(!question.required || value) && (
|
||||
{!question.required && (
|
||||
<SubmitButton
|
||||
tabIndex={question.range + 1}
|
||||
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function AutoCloseWrapper({ survey, onClose, children }: AutoCloseProps)
|
||||
}, [survey.autoClose]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-full w-full">
|
||||
{survey.autoClose && showAutoCloseProgressBar && (
|
||||
<AutoCloseProgressBar autoCloseTimeout={survey.autoClose} />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { VNode } from "preact";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
|
||||
@@ -20,7 +20,6 @@ export default function Modal({
|
||||
placement,
|
||||
clickOutside,
|
||||
darkOverlay,
|
||||
highlightBorderColor,
|
||||
onClose,
|
||||
}: ModalProps) {
|
||||
const [show, setShow] = useState(false);
|
||||
@@ -68,19 +67,6 @@ export default function Modal({
|
||||
}
|
||||
};
|
||||
|
||||
const highlightBorderColorStyle = useMemo(() => {
|
||||
if (!highlightBorderColor)
|
||||
return {
|
||||
overflow: "visible",
|
||||
};
|
||||
|
||||
return {
|
||||
borderRadius: "var(--fb-border-radius)",
|
||||
border: "2px solid",
|
||||
borderColor: highlightBorderColor,
|
||||
};
|
||||
}, [highlightBorderColor]);
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
return (
|
||||
@@ -106,26 +92,7 @@ export default function Modal({
|
||||
show ? "opacity-100" : "opacity-0",
|
||||
"rounded-custom pointer-events-auto absolute bottom-0 h-fit w-full overflow-visible bg-white shadow-lg transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
|
||||
)}>
|
||||
{!isCenter && (
|
||||
<div class="absolute right-0 top-0 z-10 block pr-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
class="text-close-button hover:text-close-button-focus focus:ring-close-button-focus relative h-5 w-5 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
<span class="sr-only">Close survey</span>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4L20 20M4 20L20 4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={highlightBorderColorStyle}>{children}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
|
||||
interface StackedCardsContainerProps {
|
||||
cardArrangement: TCardArrangementOptions;
|
||||
currentQuestionId: string;
|
||||
survey: TSurvey;
|
||||
getCardContent: (questionIdx: number, offset: number) => JSX.Element | undefined;
|
||||
styling: TProductStyling | TSurveyStyling;
|
||||
setQuestionId: (questionId: string) => void;
|
||||
}
|
||||
|
||||
export const StackedCardsContainer = ({
|
||||
cardArrangement,
|
||||
currentQuestionId,
|
||||
survey,
|
||||
getCardContent,
|
||||
styling,
|
||||
setQuestionId,
|
||||
}: StackedCardsContainerProps) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const highlightBorderColor =
|
||||
survey.styling?.highlightBorderColor?.light || styling.highlightBorderColor?.light;
|
||||
const cardBorderColor = survey.styling?.cardBorderColor?.light || styling.cardBorderColor?.light;
|
||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const resizeObserver = useRef<ResizeObserver | null>(null);
|
||||
const [cardHeight, setCardHeight] = useState("auto");
|
||||
|
||||
const cardIndexes = useMemo(() => {
|
||||
let cardIndexTemp = survey.questions.map((_, index) => index);
|
||||
if (survey.welcomeCard.enabled) {
|
||||
cardIndexTemp.unshift(-1);
|
||||
}
|
||||
if (survey.thankYouCard.enabled) {
|
||||
cardIndexTemp.push(survey.questions.length);
|
||||
}
|
||||
return cardIndexTemp;
|
||||
}, [survey]);
|
||||
|
||||
const questionIdx = useMemo(() => {
|
||||
if (currentQuestionId === "start") return survey.welcomeCard.enabled ? -1 : 0;
|
||||
if (currentQuestionId === "end") return survey.thankYouCard.enabled ? survey.questions.length : 0;
|
||||
return survey.questions.findIndex((question) => question.id === currentQuestionId);
|
||||
}, [currentQuestionId, survey.welcomeCard.enabled, survey.thankYouCard.enabled, survey.questions]);
|
||||
|
||||
const borderStyles = useMemo(() => {
|
||||
const baseStyle = {
|
||||
border: "1px solid",
|
||||
borderRadius: "var(--fb-border-radius)",
|
||||
};
|
||||
// Determine borderColor based on the survey type and availability of highlightBorderColor
|
||||
const borderColor =
|
||||
survey.type === "link" || !highlightBorderColor ? cardBorderColor : highlightBorderColor;
|
||||
return {
|
||||
...baseStyle,
|
||||
borderColor: borderColor,
|
||||
};
|
||||
}, [survey.type, cardBorderColor, highlightBorderColor]);
|
||||
|
||||
const calculateCardTransform = useMemo(() => {
|
||||
return (offset: number) => {
|
||||
switch (cardArrangement) {
|
||||
case "casual":
|
||||
return offset < 0 ? `translateX(33%)` : `translateX(0) rotate(-${(hovered ? 3.5 : 3) * offset}deg)`;
|
||||
case "straight":
|
||||
return offset < 0 ? `translateY(25%)` : `translateY(-${(hovered ? 12 : 10) * offset}px)`;
|
||||
default:
|
||||
return offset < 0 ? `translateX(0)` : `translateX(0)`;
|
||||
}
|
||||
};
|
||||
}, [cardArrangement, hovered]);
|
||||
|
||||
const straightCardArrangementStyles = (offset: number) => {
|
||||
if (cardArrangement === "straight") {
|
||||
// styles to set the descending width of stacked question cards when card arrangement is set to straight
|
||||
return {
|
||||
width: `${100 - 5 * offset >= 100 ? 100 : 100 - 5 * offset}%`,
|
||||
margin: "auto",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// UseEffect to handle the resize of current question card and set cardHeight accordingly
|
||||
useEffect(() => {
|
||||
const currentElement = cardRefs.current[questionIdx];
|
||||
if (currentElement) {
|
||||
if (resizeObserver.current) resizeObserver.current.disconnect();
|
||||
resizeObserver.current = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) setCardHeight(entry.contentRect.height + "px");
|
||||
});
|
||||
resizeObserver.current.observe(currentElement);
|
||||
}
|
||||
return () => resizeObserver.current?.disconnect();
|
||||
}, [questionIdx, cardArrangement]);
|
||||
|
||||
// Reset question progress, when card arrangement changes
|
||||
useEffect(() => {
|
||||
setQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cardArrangement]);
|
||||
|
||||
const getCardHeight = (offset: number): string => {
|
||||
// Take default height depending upon card content
|
||||
if (offset === 0) return "auto";
|
||||
// Preserve original height
|
||||
else if (offset < 0) return "initial";
|
||||
// Assign the height of the foremost card to all cards behind it
|
||||
else return cardHeight;
|
||||
};
|
||||
|
||||
const getBottomStyles = () => {
|
||||
if (survey.type !== "link")
|
||||
return {
|
||||
bottom: 0,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex items-end justify-center md:items-center"
|
||||
onMouseEnter={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setHovered(false)}>
|
||||
<div style={{ height: cardHeight }}></div>
|
||||
{cardArrangement === "simple"
|
||||
? getCardContent(questionIdx, 0)
|
||||
: questionIdx !== undefined &&
|
||||
cardIndexes.map((_, idx) => {
|
||||
const index = survey.welcomeCard.enabled ? idx - 1 : idx;
|
||||
const offset = index - questionIdx;
|
||||
const isHidden = offset < 0;
|
||||
return (
|
||||
<div
|
||||
ref={(el) => (cardRefs.current[index] = el)}
|
||||
id={`questionCard-${index}`}
|
||||
key={index}
|
||||
style={{
|
||||
zIndex: 1000 - index,
|
||||
transform: `${calculateCardTransform(offset)}`,
|
||||
opacity: isHidden ? 0 : (100 - 30 * offset) / 100,
|
||||
height: getCardHeight(offset),
|
||||
transitionDuration: "600ms",
|
||||
pointerEvents: offset === 0 ? "auto" : "none",
|
||||
...borderStyles,
|
||||
...straightCardArrangementStyles(offset),
|
||||
...getBottomStyles(),
|
||||
}}
|
||||
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 backdrop-blur-md transition-all ease-in-out">
|
||||
{getCardContent(index, offset)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// offset = 0 -> Current question card
|
||||
// offset < 0 -> Question cards that are already answered
|
||||
// offset > 0 -> Question that aren't answered yet
|
||||
@@ -23,13 +23,12 @@ export const useTtc = (
|
||||
ttc: TResponseTtc,
|
||||
setTtc: (ttc: TResponseTtc) => void,
|
||||
startTime: number,
|
||||
setStartTime: (time: number) => void
|
||||
setStartTime: (time: number) => void,
|
||||
isCurrentQuestion: boolean
|
||||
) => {
|
||||
useEffect(() => {
|
||||
setStartTime(performance.now());
|
||||
}, [questionId, setStartTime]);
|
||||
if (!isCurrentQuestion) return;
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
// Restart the timer when the tab becomes visible again
|
||||
@@ -47,5 +46,11 @@ export const useTtc = (
|
||||
// Clean up the event listener when the component is unmounted
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [questionId, setStartTime, setTtc, startTime, ttc]);
|
||||
}, [questionId, setStartTime, setTtc, startTime, ttc, isCurrentQuestion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentQuestion) {
|
||||
setStartTime(performance.now());
|
||||
}
|
||||
}, [questionId, setStartTime, isCurrentQuestion]);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ export type TCardArrangementOptions = z.infer<typeof ZCardArrangementOptions>;
|
||||
|
||||
export const ZCardArrangement = z.object({
|
||||
linkSurveys: ZCardArrangementOptions,
|
||||
inAppSurveys: ZCardArrangementOptions,
|
||||
appSurveys: ZCardArrangementOptions,
|
||||
});
|
||||
|
||||
export const ZSurveyStylingBackground = z.object({
|
||||
|
||||
@@ -1,17 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
|
||||
interface StackedCardsContainerProps {
|
||||
children: React.ReactNode;
|
||||
cardArrangement: TCardArrangementOptions;
|
||||
}
|
||||
|
||||
export const StackedCardsContainer: React.FC<StackedCardsContainerProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div className="absolute -left-2 h-[89%] w-[98%] -rotate-6 rounded-xl border border-slate-200 bg-white opacity-40 backdrop-blur-lg transition-all duration-300 ease-in-out group-hover:-mt-1.5 group-hover:-rotate-[7deg]" />
|
||||
<div className="absolute h-[93%] w-[98%] -rotate-3 rounded-xl border border-slate-200 bg-white opacity-70 backdrop-blur-md transition-all duration-200 ease-in-out group-hover:-mt-1 group-hover:-rotate-[4deg]" />
|
||||
<div className="flex scale-[0.995] flex-col items-center justify-center rounded-xl border border-slate-200 bg-white bg-opacity-70 p-16 backdrop-blur-lg transition-all duration-200 ease-in-out group-hover:scale-[1]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export const StackedCardsContainer: React.FC<StackedCardsContainerProps> = ({
|
||||
children,
|
||||
cardArrangement,
|
||||
}) => {
|
||||
switch (cardArrangement) {
|
||||
case "casual":
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div className="absolute h-full w-full -rotate-6 rounded-xl border border-slate-200 bg-white opacity-40 backdrop-blur-lg transition-all duration-300 ease-in-out group-hover:-mt-1.5 group-hover:-rotate-[7deg]" />
|
||||
<div className="absolute h-full w-full -rotate-3 rounded-xl border border-slate-200 bg-white opacity-70 backdrop-blur-md transition-all duration-200 ease-in-out group-hover:-mt-1 group-hover:-rotate-[4deg]" />
|
||||
<div className="flex scale-[0.995] flex-col items-center justify-center rounded-xl border border-slate-200 bg-white bg-opacity-70 p-16 backdrop-blur-lg transition-all duration-200 ease-in-out group-hover:scale-[1]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "straight":
|
||||
return (
|
||||
<div className="group relative">
|
||||
<div className="absolute left-[4%] h-full w-[92%] -translate-y-8 rounded-xl border border-slate-200 bg-white opacity-40 backdrop-blur-lg transition-all duration-300 ease-in-out group-hover:-mt-1.5 group-hover:-translate-y-9" />
|
||||
<div className="absolute left-[2%] h-full w-[96%] -translate-y-4 rounded-xl border border-slate-200 bg-white opacity-70 backdrop-blur-md transition-all duration-200 ease-in-out group-hover:-mt-1 group-hover:-translate-y-5" />
|
||||
<div className="flex scale-[0.995] flex-col items-center justify-center rounded-xl border border-slate-200 bg-white bg-opacity-70 p-16 backdrop-blur-lg transition-all duration-200 ease-in-out group-hover:scale-[1]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border border-slate-200 bg-white p-16 backdrop-blur-lg transition-all duration-200 ease-in-out ">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
import { TSurveyType } from "@formbricks/types/surveys";
|
||||
|
||||
import { Button } from "../../Button";
|
||||
import {
|
||||
CasualCardArrangementIcon,
|
||||
NoCardsArrangementIcon,
|
||||
StraightCardArrangementIcon,
|
||||
} from "./CardArrangementIcons";
|
||||
|
||||
type CardArrangementProps = {
|
||||
surveyType: "link" | "web";
|
||||
interface CardArrangementProps {
|
||||
surveyType: TSurveyType;
|
||||
activeCardArrangement: TCardArrangementOptions;
|
||||
setActiveCardArrangement: (arrangement: TCardArrangementOptions) => void;
|
||||
setActiveCardArrangement: (arrangement: TCardArrangementOptions, surveyType: TSurveyType) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const CardArrangement = ({
|
||||
activeCardArrangement,
|
||||
@@ -19,61 +26,54 @@ export const CardArrangement = ({
|
||||
disabled = false,
|
||||
}: CardArrangementProps) => {
|
||||
const surveyTypeDerived = useMemo(() => {
|
||||
return surveyType == "link" ? "Link" : "In App";
|
||||
return surveyType == "link" ? "Link" : "App / Website";
|
||||
}, [surveyType]);
|
||||
const cardArrangementTypes: TCardArrangementOptions[] = ["casual", "straight", "simple"];
|
||||
|
||||
const handleCardArrangementChange = (arrangement: TCardArrangementOptions) => {
|
||||
if (disabled) return;
|
||||
setActiveCardArrangement(arrangement);
|
||||
setActiveCardArrangement(arrangement, surveyType);
|
||||
};
|
||||
|
||||
const getCardArrangementIcon = (cardArrangement: string) => {
|
||||
switch (cardArrangement) {
|
||||
case "casual":
|
||||
return <CasualCardArrangementIcon />;
|
||||
case "straight":
|
||||
return <StraightCardArrangementIcon />;
|
||||
default:
|
||||
return <NoCardsArrangementIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-base font-semibold text-slate-900">
|
||||
<h3 className="text-sm font-semibold text-slate-700">
|
||||
Card Arrangement for {surveyTypeDerived} Surveys
|
||||
</h3>
|
||||
<p className="text-sm text-slate-800">
|
||||
<p className="text-xs text-slate-500">
|
||||
How funky do you want your cards in {surveyTypeDerived} Surveys
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 rounded-md border border-slate-300 bg-white p-1">
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"flex flex-1 justify-center bg-white text-center",
|
||||
activeCardArrangement === "casual" && "bg-slate-200"
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={() => handleCardArrangementChange("casual")}>
|
||||
Casual
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
onClick={() => handleCardArrangementChange("straight")}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex flex-1 justify-center bg-white text-center",
|
||||
activeCardArrangement === "straight" && "bg-slate-200"
|
||||
)}>
|
||||
Straight
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
onClick={() => handleCardArrangementChange("simple")}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex flex-1 justify-center bg-white text-center",
|
||||
activeCardArrangement === "simple" && "bg-slate-200"
|
||||
)}>
|
||||
Simple
|
||||
</Button>
|
||||
{cardArrangementTypes.map((cardArrangement) => {
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"flex flex-1 justify-center space-x-4 bg-white text-center",
|
||||
activeCardArrangement === cardArrangement && "bg-slate-200"
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={() => handleCardArrangementChange(cardArrangement)}>
|
||||
<p> {capitalizeFirstLetter(cardArrangement)}</p>
|
||||
{getCardArrangementIcon(cardArrangement)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
48
packages/ui/Styling/components/CardArrangementIcons.tsx
Normal file
48
packages/ui/Styling/components/CardArrangementIcons.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
export const CasualCardArrangementIcon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" fill="none" viewBox="0 0 17 17">
|
||||
<g fill="#1E293B" clipPath="url(#clip0_604_1310)">
|
||||
<rect
|
||||
width="13.92"
|
||||
height="10.827"
|
||||
x="-0.258"
|
||||
y="8.297"
|
||||
fillOpacity="0.25"
|
||||
rx="1.031"
|
||||
transform="rotate(-38 -.258 8.297)"></rect>
|
||||
<rect
|
||||
width="13.92"
|
||||
height="10.827"
|
||||
x="0.219"
|
||||
y="5.422"
|
||||
fillOpacity="0.5"
|
||||
rx="1.031"
|
||||
transform="rotate(-19 .219 5.422)"></rect>
|
||||
<rect width="13" height="11" x="2.168" y="2.809" rx="1.031"></rect>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_604_1310">
|
||||
<path fill="#fff" d="M0 0H17V17H0z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const StraightCardArrangementIcon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="17" fill="none" viewBox="0 0 14 17">
|
||||
<rect width="10" height="9" x="2" y="1" fill="#1E293B" fillOpacity="0.25" rx="1.031"></rect>
|
||||
<rect width="12" height="9" x="1" y="3" fill="#1E293B" fillOpacity="0.6" rx="1.031"></rect>
|
||||
<rect width="14" height="11" y="5" fill="#1E293B" rx="1.031"></rect>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoCardsArrangementIcon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="13" fill="none" viewBox="0 0 14 13">
|
||||
<rect width="14" height="11" y="1" fill="#1E293B" rx="1.031"></rect>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user