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:
Dhruwang Jariwala
2024-05-07 18:12:48 +05:30
committed by GitHub
parent 2ca38b1918
commit 3a1683eebd
40 changed files with 681 additions and 306 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 : ["", "", "", "", "", ""];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[] = [];

View File

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

View File

@@ -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} />
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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