mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
Compare commits
51 Commits
| 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[];
|
attributeClasses: TAttributeClass[];
|
||||||
responseCount: number;
|
responseCount: number;
|
||||||
membershipRole?: TMembershipRole;
|
membershipRole?: TMembershipRole;
|
||||||
|
colours: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsView({
|
export default function SettingsView({
|
||||||
@@ -28,6 +29,7 @@ export default function SettingsView({
|
|||||||
attributeClasses,
|
attributeClasses,
|
||||||
responseCount,
|
responseCount,
|
||||||
membershipRole,
|
membershipRole,
|
||||||
|
colours,
|
||||||
}: SettingsViewProps) {
|
}: SettingsViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 space-y-3 p-5">
|
<div className="mt-12 space-y-3 p-5">
|
||||||
@@ -60,7 +62,7 @@ export default function SettingsView({
|
|||||||
environmentId={environment.id}
|
environmentId={environment.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} colours={colours} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,28 @@ import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
|||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Placement from "./Placement";
|
import Placement from "./Placement";
|
||||||
|
import SurveyBgSelectorTab from "./SurveyBgSelectorTab";
|
||||||
|
|
||||||
interface StylingCardProps {
|
interface StylingCardProps {
|
||||||
localSurvey: TSurvey;
|
localSurvey: TSurvey;
|
||||||
setLocalSurvey: React.Dispatch<React.SetStateAction<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 [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { type, productOverwrites } = localSurvey;
|
const { type, productOverwrites, surveyBackground } = localSurvey;
|
||||||
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
||||||
productOverwrites ?? {};
|
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 = () => {
|
const togglePlacement = () => {
|
||||||
setLocalSurvey({
|
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 = () => {
|
const toggleHighlightBorderColor = () => {
|
||||||
setLocalSurvey({
|
setLocalSurvey({
|
||||||
...localSurvey,
|
...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) => {
|
const handleBorderColorChange = (color: string) => {
|
||||||
setLocalSurvey({
|
setLocalSurvey({
|
||||||
...localSurvey,
|
...localSurvey,
|
||||||
@@ -143,6 +198,66 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 */}
|
{/* positioning */}
|
||||||
{type !== "link" && (
|
{type !== "link" && (
|
||||||
<div className="p-3 ">
|
<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[];
|
attributeClasses: TAttributeClass[];
|
||||||
responseCount: number;
|
responseCount: number;
|
||||||
membershipRole?: TMembershipRole;
|
membershipRole?: TMembershipRole;
|
||||||
|
colours: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SurveyEditor({
|
export default function SurveyEditor({
|
||||||
@@ -33,6 +34,7 @@ export default function SurveyEditor({
|
|||||||
attributeClasses,
|
attributeClasses,
|
||||||
responseCount,
|
responseCount,
|
||||||
membershipRole,
|
membershipRole,
|
||||||
|
colours,
|
||||||
}: SurveyEditorProps): JSX.Element {
|
}: SurveyEditorProps): JSX.Element {
|
||||||
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
||||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||||
@@ -99,6 +101,7 @@ export default function SurveyEditor({
|
|||||||
attributeClasses={attributeClasses}
|
attributeClasses={attributeClasses}
|
||||||
responseCount={responseCount}
|
responseCount={responseCount}
|
||||||
membershipRole={membershipRole}
|
membershipRole={membershipRole}
|
||||||
|
colours={colours}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
|
|||||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
|
import { colours } from "@formbricks/lib/constants";
|
||||||
import SurveyEditor from "./components/SurveyEditor";
|
import SurveyEditor from "./components/SurveyEditor";
|
||||||
|
|
||||||
export const generateMetadata = async ({ params }) => {
|
export const generateMetadata = async ({ params }) => {
|
||||||
@@ -67,6 +68,7 @@ export default async function SurveysEditPage({ params }) {
|
|||||||
attributeClasses={attributeClasses}
|
attributeClasses={attributeClasses}
|
||||||
responseCount={responseCount}
|
responseCount={responseCount}
|
||||||
membershipRole={currentUserMembership?.role}
|
membershipRole={currentUserMembership?.role}
|
||||||
|
colours={colours}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Modal({
|
|||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
style={highlightBorderColorStyle}
|
style={highlightBorderColorStyle}
|
||||||
className={cn(
|
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 ",
|
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full ",
|
||||||
slidingAnimationClass
|
slidingAnimationClass
|
||||||
)}>
|
)}>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
|
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 TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
|
||||||
|
import MediaBackground from "@/app/s/[surveyId]/components/MediaBackground";
|
||||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
|
||||||
import type { TEnvironment } from "@formbricks/types/environment";
|
import type { TEnvironment } from "@formbricks/types/environment";
|
||||||
import type { TProduct } from "@formbricks/types/product";
|
import type { TProduct } from "@formbricks/types/product";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
|
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||||
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
ArrowsPointingInIcon,
|
ArrowsPointingInIcon,
|
||||||
@@ -152,6 +153,20 @@ export default function PreviewSurvey({
|
|||||||
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
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(() => {
|
useEffect(() => {
|
||||||
if (environment && environment.widgetSetupCompleted) {
|
if (environment && environment.widgetSetupCompleted) {
|
||||||
setWidgetSetupCompleted(true);
|
setWidgetSetupCompleted(true);
|
||||||
@@ -191,7 +206,7 @@ export default function PreviewSurvey({
|
|||||||
<div className="absolute right-0 top-0 m-2">
|
<div className="absolute right-0 top-0 m-2">
|
||||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||||
</div>
|
</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 */}
|
{/* 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>
|
<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" ? (
|
{previewType === "modal" ? (
|
||||||
@@ -210,23 +225,17 @@ export default function PreviewSurvey({
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="relative z-10 w-full max-w-md px-4">
|
||||||
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
|
<SurveyInline
|
||||||
ref={ContentRef}>
|
survey={survey}
|
||||||
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white py-6">
|
brandColor={brandColor}
|
||||||
<div className="w-full max-w-md px-4">
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
<SurveyInline
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
survey={survey}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
brandColor={brandColor}
|
/>
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
|
||||||
isBrandingEnabled={product.linkSurveyBranding}
|
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PreviewSurveyBgMobile>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{previewMode === "desktop" && (
|
{previewMode === "desktop" && (
|
||||||
@@ -280,20 +289,18 @@ export default function PreviewSurvey({
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-grow flex-col overflow-y-auto rounded-b-lg" ref={ContentRef}>
|
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
|
||||||
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white p-4 py-6">
|
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||||
<div className="w-full max-w-md">
|
<SurveyInline
|
||||||
<SurveyInline
|
survey={survey}
|
||||||
survey={survey}
|
brandColor={brandColor}
|
||||||
brandColor={brandColor}
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
isBrandingEnabled={product.linkSurveyBranding}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
isRedirectDisabled={true}
|
||||||
isRedirectDisabled={true}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MediaBackground>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||||
import Link from "next/link";
|
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;
|
if (!IMPRINT_URL && !PRIVACY_URL) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-10 w-full border-t border-slate-200">
|
<div
|
||||||
<div className="mx-auto max-w-lg p-3 text-center text-sm text-slate-400">
|
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 && (
|
{IMPRINT_URL && (
|
||||||
<Link href={IMPRINT_URL} target="_blank">
|
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
|
||||||
Imprint
|
Imprint
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{IMPRINT_URL && PRIVACY_URL && <span> | </span>}
|
{IMPRINT_URL && PRIVACY_URL && <span className="px-2">|</span>}
|
||||||
{PRIVACY_URL && (
|
{PRIVACY_URL && (
|
||||||
<Link href={PRIVACY_URL} target="_blank">
|
<Link href={PRIVACY_URL} target="_blank" className="hover:underline">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
|
||||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
|
||||||
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
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 { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
|
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
|
||||||
|
import { FormbricksAPI } from "@formbricks/api";
|
||||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||||
import { SurveyState } from "@formbricks/lib/surveyState";
|
import { SurveyState } from "@formbricks/lib/surveyState";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
import { TProduct } from "@formbricks/types/product";
|
||||||
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
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 { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { FormbricksAPI } from "@formbricks/api";
|
|
||||||
|
|
||||||
interface LinkSurveyProps {
|
interface LinkSurveyProps {
|
||||||
survey: TSurvey;
|
survey: TSurvey;
|
||||||
@@ -116,7 +116,7 @@ export default function LinkSurvey({
|
|||||||
|
|
||||||
return (
|
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 && (
|
{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 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 />
|
<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";
|
"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 { TProduct } from "@formbricks/types/product";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
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 { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
import { OTPInput } from "@formbricks/ui/OTPInput";
|
||||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
import type { NextPage } from "next";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface LinkSurveyPinScreenProps {
|
interface LinkSurveyPinScreenProps {
|
||||||
surveyId: string;
|
surveyId: string;
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
|
|
||||||
|
|
||||||
export default async function SurveyLayout({ children }) {
|
export default async function SurveyLayout({ children }) {
|
||||||
return (
|
return <div>{children}</div>;
|
||||||
<div className="flex h-full flex-col justify-between bg-white">
|
|
||||||
<div className="h-full overflow-y-auto">{children}</div>
|
|
||||||
<LegalFooter />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export const revalidate = REVALIDATION_INTERVAL;
|
export const revalidate = REVALIDATION_INTERVAL;
|
||||||
|
|
||||||
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||||
|
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
|
||||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
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 PinScreen from "@/app/s/[surveyId]/components/PinScreen";
|
||||||
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
|
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
|
||||||
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
|
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 { getSurvey } from "@formbricks/lib/survey/service";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
import { TResponse } from "@formbricks/types/responses";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getEmailVerificationStatus } from "./lib/helpers";
|
import { getEmailVerificationStatus } from "./lib/helpers";
|
||||||
|
|
||||||
@@ -171,16 +174,21 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return survey ? (
|
||||||
<LinkSurvey
|
<div>
|
||||||
survey={survey}
|
<MediaBackground survey={survey}>
|
||||||
product={product}
|
<LinkSurvey
|
||||||
userId={userId}
|
survey={survey}
|
||||||
emailVerificationStatus={emailVerificationStatus}
|
product={product}
|
||||||
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
userId={userId}
|
||||||
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
|
emailVerificationStatus={emailVerificationStatus}
|
||||||
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
|
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
||||||
webAppUrl={WEBAPP_URL}
|
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,
|
TSurveyClosedMessage,
|
||||||
TSurveyHiddenFields,
|
TSurveyHiddenFields,
|
||||||
TSurveyProductOverwrites,
|
TSurveyProductOverwrites,
|
||||||
|
TSurveyBackground,
|
||||||
TSurveyQuestions,
|
TSurveyQuestions,
|
||||||
TSurveySingleUse,
|
TSurveySingleUse,
|
||||||
TSurveyThankYouCard,
|
TSurveyThankYouCard,
|
||||||
@@ -27,6 +28,7 @@ declare global {
|
|||||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||||
export type SurveyHiddenFields = TSurveyHiddenFields;
|
export type SurveyHiddenFields = TSurveyHiddenFields;
|
||||||
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
||||||
|
export type SurveyBackground = TSurveyBackground;
|
||||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||||
export type SurveySingleUse = TSurveySingleUse;
|
export type SurveySingleUse = TSurveySingleUse;
|
||||||
export type SurveyVerifyEmail = TSurveyVerifyEmail;
|
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)
|
/// @zod.custom(imports.ZSurveyProductOverwrites)
|
||||||
/// [SurveyProductOverwrites]
|
/// [SurveyProductOverwrites]
|
||||||
productOverwrites Json?
|
productOverwrites Json?
|
||||||
|
/// @zod.custom(imports.ZSurveyBackground)
|
||||||
|
/// [SurveyBackground]
|
||||||
|
surveyBackground Json?
|
||||||
/// @zod.custom(imports.ZSurveySingleUse)
|
/// @zod.custom(imports.ZSurveySingleUse)
|
||||||
/// [SurveySingleUse]
|
/// [SurveySingleUse]
|
||||||
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export {
|
|||||||
ZSurveyHiddenFields,
|
ZSurveyHiddenFields,
|
||||||
ZSurveyClosedMessage,
|
ZSurveyClosedMessage,
|
||||||
ZSurveyProductOverwrites,
|
ZSurveyProductOverwrites,
|
||||||
|
ZSurveyBackground,
|
||||||
ZSurveyVerifyEmail,
|
ZSurveyVerifyEmail,
|
||||||
ZSurveySingleUse,
|
ZSurveySingleUse,
|
||||||
} from "@formbricks/types/surveys";
|
} from "@formbricks/types/surveys";
|
||||||
|
|||||||
@@ -75,6 +75,34 @@ export const LOCAL_UPLOAD_URL = {
|
|||||||
export const PRICING_USERTARGETING_FREE_MTU = 2500;
|
export const PRICING_USERTARGETING_FREE_MTU = 2500;
|
||||||
export const PRICING_APPSURVEYS_FREE_RESPONSES = 250;
|
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
|
// Rate Limiting
|
||||||
export const SIGNUP_RATE_LIMIT = {
|
export const SIGNUP_RATE_LIMIT = {
|
||||||
interval: 60 * 60 * 1000, // 60 minutes
|
interval: 60 * 60 * 1000, // 60 minutes
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const selectSurvey = {
|
|||||||
verifyEmail: true,
|
verifyEmail: true,
|
||||||
redirectUrl: true,
|
redirectUrl: true,
|
||||||
productOverwrites: true,
|
productOverwrites: true,
|
||||||
|
surveyBackground: true,
|
||||||
surveyClosedMessage: true,
|
surveyClosedMessage: true,
|
||||||
singleUse: true,
|
singleUse: true,
|
||||||
pin: true,
|
pin: true,
|
||||||
@@ -585,6 +586,9 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
|
|||||||
productOverwrites: existingSurvey.productOverwrites
|
productOverwrites: existingSurvey.productOverwrites
|
||||||
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
||||||
: Prisma.JsonNull,
|
: Prisma.JsonNull,
|
||||||
|
surveyBackground: existingSurvey.surveyBackground
|
||||||
|
? JSON.parse(JSON.stringify(existingSurvey.surveyBackground))
|
||||||
|
: Prisma.JsonNull,
|
||||||
verifyEmail: existingSurvey.verifyEmail
|
verifyEmail: existingSurvey.verifyEmail
|
||||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||||
: Prisma.JsonNull,
|
: Prisma.JsonNull,
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ export default function Headline({
|
|||||||
}: HeadlineProps) {
|
}: HeadlineProps) {
|
||||||
return (
|
return (
|
||||||
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
||||||
<div
|
<div className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}>
|
||||||
className={`flex items-center ${alignTextCenter ? "justify-center" : "mr-[3ch] justify-between"}`}>
|
|
||||||
{headline}
|
{headline}
|
||||||
{!required && (
|
{!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
|
Optional
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function Survey({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
<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")}>
|
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
|
||||||
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
|
{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
|
// 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(),
|
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 TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
|
||||||
|
|
||||||
|
export type TSurveyBackground = z.infer<typeof ZSurveyBackground>;
|
||||||
|
|
||||||
export const ZSurveyClosedMessage = z
|
export const ZSurveyClosedMessage = z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -359,6 +367,7 @@ export const ZSurvey = z.object({
|
|||||||
autoComplete: z.number().nullable(),
|
autoComplete: z.number().nullable(),
|
||||||
closeOnDate: z.date().nullable(),
|
closeOnDate: z.date().nullable(),
|
||||||
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
||||||
|
surveyBackground: ZSurveyBackground.nullable(),
|
||||||
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
|
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
|
||||||
singleUse: ZSurveySingleUse.nullable(),
|
singleUse: ZSurveySingleUse.nullable(),
|
||||||
verifyEmail: ZSurveyVerifyEmail.nullable(),
|
verifyEmail: ZSurveyVerifyEmail.nullable(),
|
||||||
|
|||||||
Reference in New Issue
Block a user