mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-21 13:40:31 -06:00
Compare commits
51 Commits
add-cursor
...
surveyBg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d21be5fa | ||
|
|
5c47bd00d7 | ||
|
|
6091413d82 | ||
|
|
d8e1e103ee | ||
|
|
a063f920be | ||
|
|
ab4c8838f9 | ||
|
|
7d2fffbdab | ||
|
|
b14d4e4235 | ||
|
|
52659b14ac | ||
|
|
cbe610aa90 | ||
|
|
d4a302fb20 | ||
|
|
28a33ee818 | ||
|
|
870ba09abe | ||
|
|
39b11f9dba | ||
|
|
71cfd31b7a | ||
|
|
b47eeb4673 | ||
|
|
708bfd8810 | ||
|
|
e6e41b81a3 | ||
|
|
e7d8a675c9 | ||
|
|
12e4cc110c | ||
|
|
fa7f7c497c | ||
|
|
4066fbf7d6 | ||
|
|
16245fe355 | ||
|
|
057d2ec446 | ||
|
|
9f80ed6ec2 | ||
|
|
90b0734cd2 | ||
|
|
6711543cdf | ||
|
|
862e72ca8f | ||
|
|
14c249cbde | ||
|
|
1bded4cce1 | ||
|
|
bbc6dfbc97 | ||
|
|
351ade6448 | ||
|
|
a530017c3b | ||
|
|
0479c34be9 | ||
|
|
e0e5ca93b6 | ||
|
|
c80dde1ba1 | ||
|
|
c9ba9e8e1b | ||
|
|
fd5284025f | ||
|
|
2213adb24a | ||
|
|
8d028cdf45 | ||
|
|
e705fcf763 | ||
|
|
7f65b41b59 | ||
|
|
0809055247 | ||
|
|
8546ce918c | ||
|
|
3cf1e7ad3b | ||
|
|
c7e2e08026 | ||
|
|
f39b15790b | ||
|
|
aa9142b3c0 | ||
|
|
ca634cd798 | ||
|
|
f8518fcd80 | ||
|
|
2e2ad05367 |
@@ -0,0 +1,107 @@
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AnimatedSurveyBgProps {
|
||||
localSurvey?: TSurvey;
|
||||
handleBgChange: (bg: string, bgType: string) => void;
|
||||
}
|
||||
|
||||
export default function AnimatedSurveyBg({ localSurvey, handleBgChange }: AnimatedSurveyBgProps) {
|
||||
const [color, setColor] = useState(localSurvey?.surveyBackground?.bg || "#ffff");
|
||||
const [hoveredVideo, setHoveredVideo] = useState<number | null>(null);
|
||||
|
||||
const animationFiles = {
|
||||
"/animated-bgs/Thumbnails/1_Thumb.mp4": "/animated-bgs/4K/1_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/2_Thumb.mp4": "/animated-bgs/4K/2_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/3_Thumb.mp4": "/animated-bgs/4K/3_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/4_Thumb.mp4": "/animated-bgs/4K/4_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/5_Thumb.mp4": "/animated-bgs/4K/5_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/6_Thumb.mp4": "/animated-bgs/4K/6_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/7_Thumb.mp4": "/animated-bgs/4K/7_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/8_Thumb.mp4": "/animated-bgs/4K/8_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/9_Thumb.mp4": "/animated-bgs/4K/9_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/10_Thumb.mp4": "/animated-bgs/4K/10_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/11_Thumb.mp4": "/animated-bgs/4K/11_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/12_Thumb.mp4": "/animated-bgs/4K/12_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/13_Thumb.mp4": "/animated-bgs/4K/13_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/14_Thumb.mp4": "/animated-bgs/4K/14_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/15_Thumb.mp4": "/animated-bgs/4K/15_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/16_Thumb.mp4": "/animated-bgs/4K/16_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/17_Thumb.mp4": "/animated-bgs/4K/17_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/18_Thumb.mp4": "/animated-bgs/4K/18_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/19_Thumb.mp4": "/animated-bgs/4K/19_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/20_Thumb.mp4": "/animated-bgs/4K/20_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/21_Thumb.mp4": "/animated-bgs/4K/21_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/22_Thumb.mp4": "/animated-bgs/4K/22_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/23_Thumb.mp4": "/animated-bgs/4K/23_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/24_Thumb.mp4": "/animated-bgs/4K/24_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/25_Thumb.mp4": "/animated-bgs/4K/25_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/26_Thumb.mp4": "/animated-bgs/4K/26_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/27_Thumb.mp4": "/animated-bgs/4K/27_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/28_Thumb.mp4": "/animated-bgs/4K/28_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/29_Thumb.mp4": "/animated-bgs/4K/29_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/30_Thumb.mp4": "/animated-bgs/4K/30_4k.mp4",
|
||||
};
|
||||
|
||||
const handleMouseEnter = (index: number) => {
|
||||
setHoveredVideo(index);
|
||||
playVideo(index);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (index: number) => {
|
||||
setHoveredVideo(null);
|
||||
pauseVideo(index);
|
||||
};
|
||||
|
||||
// Function to play the video
|
||||
const playVideo = (index: number) => {
|
||||
const video = document.getElementById(`video-${index}`) as HTMLVideoElement;
|
||||
if (video) {
|
||||
video.play();
|
||||
}
|
||||
};
|
||||
|
||||
// Function to pause the video
|
||||
const pauseVideo = (index: number) => {
|
||||
const video = document.getElementById(`video-${index}`) as HTMLVideoElement;
|
||||
if (video) {
|
||||
video.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBg = (x: string) => {
|
||||
setColor(x);
|
||||
handleBgChange(x, "animation");
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-4 grid grid-cols-6 gap-4">
|
||||
{Object.keys(animationFiles).map((key, index) => {
|
||||
const value = animationFiles[key];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
onMouseEnter={() => handleMouseEnter(index)}
|
||||
onMouseLeave={() => handleMouseLeave(index)}
|
||||
onClick={() => handleBg(value)}
|
||||
className="relative cursor-pointer overflow-hidden rounded-lg">
|
||||
<video
|
||||
disablePictureInPicture
|
||||
id={`video-${index}`}
|
||||
autoPlay={hoveredVideo === index}
|
||||
className="h-46 w-96 origin-center scale-105 transform">
|
||||
<source src={`${key}`} type="video/mp4" />
|
||||
</video>
|
||||
<input
|
||||
className="absolute right-2 top-2 h-4 w-4 rounded-sm bg-white "
|
||||
type="checkbox"
|
||||
checked={color === value}
|
||||
onChange={() => handleBg(value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ColorSurveyBgBgProps {
|
||||
localSurvey?: TSurvey;
|
||||
handleBgChange: (bg: string, bgType: string) => void;
|
||||
colours: string[];
|
||||
}
|
||||
|
||||
export default function ColorSurveyBg({ localSurvey, handleBgChange, colours }: ColorSurveyBgBgProps) {
|
||||
const [color, setColor] = useState(localSurvey?.surveyBackground?.bg || "#ffff");
|
||||
|
||||
const handleBg = (x: string) => {
|
||||
setColor(x);
|
||||
handleBgChange(x, "color");
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className="w-full max-w-xs py-2">
|
||||
<ColorPicker color={color} onChange={handleBg} />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4 md:grid-cols-5 xl:grid-cols-8 2xl:grid-cols-10">
|
||||
{colours.map((x) => {
|
||||
return (
|
||||
<div
|
||||
className={`h-16 w-16 cursor-pointer rounded-lg ${
|
||||
color === x ? "border-4 border-slate-500" : ""
|
||||
}`}
|
||||
key={x}
|
||||
style={{ backgroundColor: `${x}` }}
|
||||
onClick={() => handleBg(x)}></div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import FileInput from "@formbricks/ui/FileInput";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
interface ImageSurveyBgBgProps {
|
||||
localSurvey?: TSurvey;
|
||||
handleBgChange: (url: string, bgType: string) => void;
|
||||
}
|
||||
|
||||
export default function ImageSurveyBg({ localSurvey, handleBgChange }: ImageSurveyBgBgProps) {
|
||||
const isUrl = (str: string) => {
|
||||
try {
|
||||
new URL(str);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const fileUrl = isUrl(localSurvey?.surveyBackground?.bg ?? "")
|
||||
? localSurvey?.surveyBackground?.bg ?? ""
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div className="mb-2 mt-4 w-full rounded-lg border bg-slate-50 p-4">
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="survey-bg-file-input"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
environmentId={localSurvey?.environmentId}
|
||||
onFileUpload={(url: string[]) => {
|
||||
if (url.length > 0) {
|
||||
handleBgChange(url[0], "image");
|
||||
} else {
|
||||
handleBgChange("#ffff", "color");
|
||||
}
|
||||
}}
|
||||
fileUrl={fileUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ interface SettingsViewProps {
|
||||
attributeClasses: TAttributeClass[];
|
||||
responseCount: number;
|
||||
membershipRole?: TMembershipRole;
|
||||
colours: string[];
|
||||
}
|
||||
|
||||
export default function SettingsView({
|
||||
@@ -28,6 +29,7 @@ export default function SettingsView({
|
||||
attributeClasses,
|
||||
responseCount,
|
||||
membershipRole,
|
||||
colours,
|
||||
}: SettingsViewProps) {
|
||||
return (
|
||||
<div className="mt-12 space-y-3 p-5">
|
||||
@@ -60,7 +62,7 @@ export default function SettingsView({
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
|
||||
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} colours={colours} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,18 +9,28 @@ import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
import Placement from "./Placement";
|
||||
import SurveyBgSelectorTab from "./SurveyBgSelectorTab";
|
||||
|
||||
interface StylingCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
|
||||
colours: string[];
|
||||
}
|
||||
|
||||
export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCardProps) {
|
||||
export default function StylingCard({ localSurvey, setLocalSurvey, colours }: StylingCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { type, productOverwrites } = localSurvey;
|
||||
const { type, productOverwrites, surveyBackground } = localSurvey;
|
||||
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
||||
productOverwrites ?? {};
|
||||
const { bg, bgType, brightness } = surveyBackground ?? {};
|
||||
|
||||
const [inputValue, setInputValue] = useState(100);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setInputValue(e.target.value);
|
||||
handleBrightnessChange(parseInt(e.target.value));
|
||||
};
|
||||
|
||||
const togglePlacement = () => {
|
||||
setLocalSurvey({
|
||||
@@ -42,6 +52,28 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
||||
});
|
||||
};
|
||||
|
||||
const toggleBackgroundColor = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
surveyBackground: {
|
||||
...localSurvey.surveyBackground,
|
||||
bg: !!bg ? undefined : "#ffff",
|
||||
bgType: !!bg ? undefined : "color",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleBrightness = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
surveyBackground: {
|
||||
...localSurvey.surveyBackground,
|
||||
brightness: !!brightness ? undefined : 100,
|
||||
},
|
||||
});
|
||||
setInputValue(100);
|
||||
};
|
||||
|
||||
const toggleHighlightBorderColor = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
@@ -62,6 +94,29 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
||||
});
|
||||
};
|
||||
|
||||
const handleBgChange = (color: string, type: string) => {
|
||||
setInputValue(100);
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
surveyBackground: {
|
||||
...localSurvey.surveyBackground,
|
||||
bg: color,
|
||||
bgType: type,
|
||||
brightness: brightness !== undefined ? 100 : brightness,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBrightnessChange = (percent: number) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
surveyBackground: {
|
||||
...localSurvey.surveyBackground,
|
||||
brightness: percent,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBorderColorChange = (color: string) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
@@ -143,6 +198,66 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{type == "link" && (
|
||||
<>
|
||||
{/* Background */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="autoCompleteBg" checked={!!bg} onCheckedChange={toggleBackgroundColor} />
|
||||
<Label htmlFor="autoCompleteBg" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Change Background</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Pick a background from our library or upload your own.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{bg && (
|
||||
<SurveyBgSelectorTab
|
||||
localSurvey={localSurvey}
|
||||
handleBgChange={handleBgChange}
|
||||
colours={colours}
|
||||
bgType={bgType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Overlay */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="autoCompleteOverlay"
|
||||
checked={!!brightness}
|
||||
onCheckedChange={toggleBrightness}
|
||||
/>
|
||||
<Label htmlFor="autoCompleteOverlay" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Background Overlay</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Darken or lighten background of your choice.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{brightness && (
|
||||
<div>
|
||||
<div className="mt-4 flex flex-col justify-center rounded-lg border bg-slate-50 p-4 px-8">
|
||||
<h3 className="mb-4 text-sm font-semibold text-slate-700">Transparency</h3>
|
||||
<input
|
||||
id="small-range"
|
||||
type="range"
|
||||
min="1"
|
||||
max="200"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
className="range-sm mb-6 h-1 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* positioning */}
|
||||
{type !== "link" && (
|
||||
<div className="p-3 ">
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { useState } from "react";
|
||||
import AnimatedSurveyBg from "./AnimatedSurveyBg";
|
||||
import ColorSurveyBg from "./ColorSurveyBg";
|
||||
import ImageSurveyBg from "./ImageSurveyBg";
|
||||
|
||||
interface SurveyBgSelectorTabProps {
|
||||
localSurvey: TSurvey;
|
||||
handleBgChange: (bg: string, bgType: string) => void;
|
||||
colours: string[];
|
||||
bgType: string | null | undefined;
|
||||
}
|
||||
|
||||
const TabButton = ({ isActive, onClick, children }) => (
|
||||
<button
|
||||
className={`w-1/4 rounded-md p-2 text-sm font-medium leading-none text-slate-800 ${
|
||||
isActive ? "bg-white shadow-sm" : ""
|
||||
}`}
|
||||
onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default function SurveyBgSelectorTab({
|
||||
localSurvey,
|
||||
handleBgChange,
|
||||
colours,
|
||||
bgType,
|
||||
}: SurveyBgSelectorTabProps) {
|
||||
const [tab, setTab] = useState(bgType || "image");
|
||||
|
||||
const renderContent = () => {
|
||||
switch (tab) {
|
||||
case "image":
|
||||
return <ImageSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} />;
|
||||
case "animation":
|
||||
return <AnimatedSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} />;
|
||||
case "color":
|
||||
return <ColorSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} colours={colours} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col items-center justify-center rounded-lg border bg-slate-50 p-4 px-8">
|
||||
<div className="flex w-full items-center justify-between rounded-lg border border-slate-300 bg-slate-50 px-6 py-1.5">
|
||||
<TabButton isActive={tab === "image"} onClick={() => setTab("image")}>
|
||||
Image
|
||||
</TabButton>
|
||||
<TabButton isActive={tab === "animation"} onClick={() => setTab("animation")}>
|
||||
Animation
|
||||
</TabButton>
|
||||
<TabButton isActive={tab === "color"} onClick={() => setTab("color")}>
|
||||
Color
|
||||
</TabButton>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,7 @@ interface SurveyEditorProps {
|
||||
attributeClasses: TAttributeClass[];
|
||||
responseCount: number;
|
||||
membershipRole?: TMembershipRole;
|
||||
colours: string[];
|
||||
}
|
||||
|
||||
export default function SurveyEditor({
|
||||
@@ -33,6 +34,7 @@ export default function SurveyEditor({
|
||||
attributeClasses,
|
||||
responseCount,
|
||||
membershipRole,
|
||||
colours,
|
||||
}: SurveyEditorProps): JSX.Element {
|
||||
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||
@@ -99,6 +101,7 @@ export default function SurveyEditor({
|
||||
attributeClasses={attributeClasses}
|
||||
responseCount={responseCount}
|
||||
membershipRole={membershipRole}
|
||||
colours={colours}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { colours } from "@formbricks/lib/constants";
|
||||
import SurveyEditor from "./components/SurveyEditor";
|
||||
|
||||
export const generateMetadata = async ({ params }) => {
|
||||
@@ -67,6 +68,7 @@ export default async function SurveysEditPage({ params }) {
|
||||
attributeClasses={attributeClasses}
|
||||
responseCount={responseCount}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
colours={colours}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Modal({
|
||||
ref={modalRef}
|
||||
style={highlightBorderColorStyle}
|
||||
className={cn(
|
||||
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out",
|
||||
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out ",
|
||||
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full ",
|
||||
slidingAnimationClass
|
||||
)}>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
|
||||
import PreviewSurveyBgMobile from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurveyBgMobile";
|
||||
import TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
|
||||
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
import MediaBackground from "@/app/s/[surveyId]/components/MediaBackground";
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
@@ -152,6 +153,20 @@ export default function PreviewSurvey({
|
||||
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
}
|
||||
|
||||
function animationTrigger() {
|
||||
let storePreviewMode = previewMode;
|
||||
setPreviewMode("null");
|
||||
setTimeout(() => {
|
||||
setPreviewMode(storePreviewMode);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (survey.surveyBackground?.bgType === "animation") {
|
||||
animationTrigger();
|
||||
}
|
||||
}, [survey.surveyBackground?.bg]);
|
||||
|
||||
useEffect(() => {
|
||||
if (environment && environment.widgetSetupCompleted) {
|
||||
setWidgetSetupCompleted(true);
|
||||
@@ -191,7 +206,7 @@ export default function PreviewSurvey({
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||
</div>
|
||||
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500 bg-slate-400">
|
||||
<PreviewSurveyBgMobile survey={survey} ContentRef={ContentRef}>
|
||||
{/* below element is use to create notch for the mobile device mockup */}
|
||||
<div className="absolute left-1/2 right-1/2 top-0 z-20 h-4 w-1/2 -translate-x-1/2 transform rounded-b-md bg-slate-500"></div>
|
||||
{previewType === "modal" ? (
|
||||
@@ -210,23 +225,17 @@ export default function PreviewSurvey({
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
<div
|
||||
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
|
||||
ref={ContentRef}>
|
||||
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white py-6">
|
||||
<div className="w-full max-w-md px-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 w-full max-w-md px-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PreviewSurveyBgMobile>
|
||||
</>
|
||||
)}
|
||||
{previewMode === "desktop" && (
|
||||
@@ -280,20 +289,18 @@ export default function PreviewSurvey({
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
<div className="flex flex-grow flex-col overflow-y-auto rounded-b-lg" ref={ContentRef}>
|
||||
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white p-4 py-6">
|
||||
<div className="w-full max-w-md">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MediaBackground>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
export default function PreviewSurveyBgMobile({ children, survey, ContentRef }) {
|
||||
return survey.surveyBackground && survey.surveyBackground.bgType === "color" ? (
|
||||
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
|
||||
<div
|
||||
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col"
|
||||
ref={ContentRef}
|
||||
style={{
|
||||
backgroundColor: survey.surveyBackground.bg,
|
||||
filter: survey.surveyBackground?.brightness
|
||||
? `brightness(${survey.surveyBackground.brightness}%)`
|
||||
: "none",
|
||||
}}></div>
|
||||
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
) : survey.surveyBackground && survey.surveyBackground.bgType === "animation" ? (
|
||||
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
|
||||
<div
|
||||
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
|
||||
ref={ContentRef}>
|
||||
<div className="relative flex w-full flex-grow flex-col items-center justify-center">
|
||||
<div className="absolute inset-0 h-full w-full object-cover">
|
||||
<video
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
className="h-full w-full object-cover"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
filter: survey.surveyBackground?.brightness
|
||||
? `brightness(${survey.surveyBackground.brightness}%)`
|
||||
: "none",
|
||||
}}>
|
||||
<source src={survey.surveyBackground.bg} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : survey.surveyBackground && survey.surveyBackground.bgType === "image" ? (
|
||||
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
|
||||
<div
|
||||
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
|
||||
ref={ContentRef}
|
||||
style={{
|
||||
backgroundImage: `url(${survey.surveyBackground.bg})`,
|
||||
filter: survey.surveyBackground?.brightness
|
||||
? `brightness(${survey.surveyBackground.brightness}%)`
|
||||
: "none",
|
||||
}}></div>
|
||||
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
|
||||
<div
|
||||
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col"
|
||||
ref={ContentRef}
|
||||
style={{
|
||||
backgroundColor: "#ffff",
|
||||
filter: survey.surveyBackground?.brightness
|
||||
? `brightness(${survey.surveyBackground.brightness}%)`
|
||||
: "none",
|
||||
}}></div>
|
||||
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function LegalFooter() {
|
||||
interface LegalFooterProps {
|
||||
bgColor?: string | null;
|
||||
}
|
||||
|
||||
export default function LegalFooter({ bgColor }: LegalFooterProps) {
|
||||
if (!IMPRINT_URL && !PRIVACY_URL) return null;
|
||||
|
||||
return (
|
||||
<div className="h-10 w-full border-t border-slate-200">
|
||||
<div className="mx-auto max-w-lg p-3 text-center text-sm text-slate-400">
|
||||
<div
|
||||
className={`fixed bottom-0 h-12 w-full`}
|
||||
style={{
|
||||
backgroundColor: `${bgColor}`,
|
||||
}}>
|
||||
<div className="mx-auto max-w-lg p-3 text-center text-xs text-slate-400">
|
||||
{IMPRINT_URL && (
|
||||
<Link href={IMPRINT_URL} target="_blank">
|
||||
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
|
||||
Imprint
|
||||
</Link>
|
||||
)}
|
||||
{IMPRINT_URL && PRIVACY_URL && <span> | </span>}
|
||||
{IMPRINT_URL && PRIVACY_URL && <span className="px-2">|</span>}
|
||||
{PRIVACY_URL && (
|
||||
<Link href={PRIVACY_URL} target="_blank">
|
||||
<Link href={PRIVACY_URL} target="_blank" className="hover:underline">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
||||
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
|
||||
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||
import { SurveyState } from "@formbricks/lib/surveyState";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
|
||||
interface LinkSurveyProps {
|
||||
survey: TSurvey;
|
||||
@@ -116,7 +116,7 @@ export default function LinkSurvey({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentWrapper className="h-full w-full p-0 md:max-w-lg">
|
||||
<ContentWrapper className="h-full w-full p-0 md:max-w-md">
|
||||
{isPreview && (
|
||||
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
|
||||
<div />
|
||||
|
||||
91
apps/web/app/s/[surveyId]/components/MediaBackground.tsx
Normal file
91
apps/web/app/s/[surveyId]/components/MediaBackground.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import React from "react";
|
||||
|
||||
const MediaBackground = ({
|
||||
children,
|
||||
survey,
|
||||
isEditorView,
|
||||
ContentRef,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
survey: TSurvey;
|
||||
isEditorView?: boolean;
|
||||
ContentRef?: React.RefObject<HTMLDivElement>;
|
||||
}) => {
|
||||
const getFilterStyle = () => {
|
||||
return survey.surveyBackground?.brightness
|
||||
? { filter: `brightness(${survey.surveyBackground.brightness}%)` }
|
||||
: {};
|
||||
};
|
||||
|
||||
const renderBackground = () => {
|
||||
const filterStyle = getFilterStyle();
|
||||
|
||||
switch (survey.surveyBackground?.bgType) {
|
||||
case "color":
|
||||
// Ensure backgroundColor is applied directly
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...filterStyle,
|
||||
backgroundColor: survey.surveyBackground.bg || "#ffff", // Default color
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
className="absolute inset-0"
|
||||
/>
|
||||
);
|
||||
case "animation":
|
||||
return (
|
||||
<video
|
||||
muted
|
||||
loop
|
||||
autoPlay
|
||||
style={filterStyle}
|
||||
className="absolute inset-0 h-full w-full object-cover">
|
||||
<source src={survey.surveyBackground.bg || ""} type="video/mp4" />
|
||||
</video>
|
||||
);
|
||||
case "image":
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...filterStyle,
|
||||
backgroundImage: `url(${survey.surveyBackground.bg})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div style={{ backgroundColor: "#ffff" }} className="absolute inset-0 h-full w-full" />;
|
||||
}
|
||||
};
|
||||
|
||||
const commonClasses = "relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6";
|
||||
const previewClasses = "flex flex-grow flex-col overflow-y-auto rounded-b-lg";
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditorView ? (
|
||||
<div className={previewClasses} ref={ContentRef}>
|
||||
<div className={commonClasses}>
|
||||
{renderBackground()}
|
||||
<div className="flex h-full w-full items-center justify-center">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`flex min-h-screen flex-col items-center justify-center px-2`}>
|
||||
{renderBackground()}
|
||||
<div className="relative w-full">{children}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediaBackground;
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import type { NextPage } from "next";
|
||||
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
|
||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { OTPInput } from "@formbricks/ui/OTPInput";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { OTPInput } from "@formbricks/ui/OTPInput";
|
||||
import type { NextPage } from "next";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
interface LinkSurveyPinScreenProps {
|
||||
surveyId: string;
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
|
||||
|
||||
export default async function SurveyLayout({ children }) {
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-between bg-white">
|
||||
<div className="h-full overflow-y-auto">{children}</div>
|
||||
<LegalFooter />
|
||||
</div>
|
||||
);
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
|
||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import MediaBackground from "@/app/s/[surveyId]/components/MediaBackground";
|
||||
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
|
||||
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
|
||||
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
@@ -12,6 +14,7 @@ import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { getEmailVerificationStatus } from "./lib/helpers";
|
||||
|
||||
@@ -171,16 +174,21 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
product={product}
|
||||
userId={userId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
||||
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
|
||||
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
/>
|
||||
);
|
||||
return survey ? (
|
||||
<div>
|
||||
<MediaBackground survey={survey}>
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
product={product}
|
||||
userId={userId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
||||
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
|
||||
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
/>
|
||||
</MediaBackground>
|
||||
<LegalFooter bgColor={survey.surveyBackground?.bg || "#ffff"} />
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TSurveyClosedMessage,
|
||||
TSurveyHiddenFields,
|
||||
TSurveyProductOverwrites,
|
||||
TSurveyBackground,
|
||||
TSurveyQuestions,
|
||||
TSurveySingleUse,
|
||||
TSurveyThankYouCard,
|
||||
@@ -27,6 +28,7 @@ declare global {
|
||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||
export type SurveyHiddenFields = TSurveyHiddenFields;
|
||||
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
||||
export type SurveyBackground = TSurveyBackground;
|
||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||
export type SurveySingleUse = TSurveySingleUse;
|
||||
export type SurveyVerifyEmail = TSurveyVerifyEmail;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Survey" ADD COLUMN "surveyBackground" JSONB;
|
||||
@@ -284,6 +284,9 @@ model Survey {
|
||||
/// @zod.custom(imports.ZSurveyProductOverwrites)
|
||||
/// [SurveyProductOverwrites]
|
||||
productOverwrites Json?
|
||||
/// @zod.custom(imports.ZSurveyBackground)
|
||||
/// [SurveyBackground]
|
||||
surveyBackground Json?
|
||||
/// @zod.custom(imports.ZSurveySingleUse)
|
||||
/// [SurveySingleUse]
|
||||
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
||||
|
||||
@@ -13,6 +13,7 @@ export {
|
||||
ZSurveyHiddenFields,
|
||||
ZSurveyClosedMessage,
|
||||
ZSurveyProductOverwrites,
|
||||
ZSurveyBackground,
|
||||
ZSurveyVerifyEmail,
|
||||
ZSurveySingleUse,
|
||||
} from "@formbricks/types/surveys";
|
||||
|
||||
@@ -75,6 +75,34 @@ export const LOCAL_UPLOAD_URL = {
|
||||
export const PRICING_USERTARGETING_FREE_MTU = 2500;
|
||||
export const PRICING_APPSURVEYS_FREE_RESPONSES = 250;
|
||||
|
||||
// Colors for Survey Bg
|
||||
export const colours = [
|
||||
"#FFF2D8",
|
||||
"#EAD7BB",
|
||||
"#BCA37F",
|
||||
"#113946",
|
||||
"#04364A",
|
||||
"#176B87",
|
||||
"#64CCC5",
|
||||
"#DAFFFB",
|
||||
"#132043",
|
||||
"#1F4172",
|
||||
"#F1B4BB",
|
||||
"#FDF0F0",
|
||||
"#001524",
|
||||
"#445D48",
|
||||
"#D6CC99",
|
||||
"#FDE5D4",
|
||||
"#BEADFA",
|
||||
"#D0BFFF",
|
||||
"#DFCCFB",
|
||||
"#FFF8C9",
|
||||
"#FF8080",
|
||||
"#FFCF96",
|
||||
"#F6FDC3",
|
||||
"#CDFAD5",
|
||||
];
|
||||
|
||||
// Rate Limiting
|
||||
export const SIGNUP_RATE_LIMIT = {
|
||||
interval: 60 * 60 * 1000, // 60 minutes
|
||||
|
||||
@@ -44,6 +44,7 @@ export const selectSurvey = {
|
||||
verifyEmail: true,
|
||||
redirectUrl: true,
|
||||
productOverwrites: true,
|
||||
surveyBackground: true,
|
||||
surveyClosedMessage: true,
|
||||
singleUse: true,
|
||||
pin: true,
|
||||
@@ -585,6 +586,9 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
||||
: Prisma.JsonNull,
|
||||
surveyBackground: existingSurvey.surveyBackground
|
||||
? JSON.parse(JSON.stringify(existingSurvey.surveyBackground))
|
||||
: Prisma.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail
|
||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||
: Prisma.JsonNull,
|
||||
|
||||
@@ -13,11 +13,10 @@ export default function Headline({
|
||||
}: HeadlineProps) {
|
||||
return (
|
||||
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
||||
<div
|
||||
className={`flex items-center ${alignTextCenter ? "justify-center" : "mr-[3ch] justify-between"}`}>
|
||||
<div className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}>
|
||||
{headline}
|
||||
{!required && (
|
||||
<span className="text-info-text self-start text-sm font-normal leading-7" tabIndex={-1}>
|
||||
<span className="text-info-text ml-2 self-start text-sm font-normal leading-7" tabIndex={-1}>
|
||||
Optional
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -166,7 +166,7 @@ export function Survey({
|
||||
return (
|
||||
<>
|
||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
||||
<div className="flex h-full w-full flex-col justify-between bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
|
||||
<div className="flex h-full w-full flex-col justify-between rounded-lg bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
|
||||
<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
|
||||
|
||||
@@ -41,8 +41,16 @@ export const ZSurveyProductOverwrites = z.object({
|
||||
darkOverlay: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export const ZSurveyBackground = z.object({
|
||||
bg: z.string().nullish(),
|
||||
bgType: z.string().nullish(),
|
||||
brightness: z.number().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
|
||||
|
||||
export type TSurveyBackground = z.infer<typeof ZSurveyBackground>;
|
||||
|
||||
export const ZSurveyClosedMessage = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -359,6 +367,7 @@ export const ZSurvey = z.object({
|
||||
autoComplete: z.number().nullable(),
|
||||
closeOnDate: z.date().nullable(),
|
||||
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
||||
surveyBackground: ZSurveyBackground.nullable(),
|
||||
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
|
||||
singleUse: ZSurveySingleUse.nullable(),
|
||||
verifyEmail: ZSurveyVerifyEmail.nullable(),
|
||||
|
||||
Reference in New Issue
Block a user