From 9271e375af1293c2c166c5e2172be9ae23f7ff4b Mon Sep 17 00:00:00 2001 From: Anjy Gupta <92802904+anjy7@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:52:28 +0530 Subject: [PATCH] feat: add image/color/animation as survey background (#1515) Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes Co-authored-by: Neil Chauhan Co-authored-by: Matthias Nannt --- .../environments/[environmentId]/actions.ts | 1 + .../edit/components/AnimatedSurveyBg.tsx | 107 ++++++++++++++ .../edit/components/ColorSurveyBg.tsx | 38 +++++ .../edit/components/ImageSurveyBg.tsx | 42 ++++++ .../edit/components/SettingsView.tsx | 4 +- .../edit/components/StylingCard.tsx | 133 +++++++++++++++++- .../edit/components/SurveyBgSelectorTab.tsx | 61 ++++++++ .../edit/components/SurveyEditor.tsx | 3 + .../surveys/[surveyId]/edit/page.tsx | 2 + .../surveys/components/Modal.tsx | 2 +- .../surveys/components/PreviewSurvey.tsx | 74 +++++----- .../surveys/templates/templates.ts | 1 + .../s/[surveyId]/components/LegalFooter.tsx | 21 ++- .../s/[surveyId]/components/LinkSurvey.tsx | 10 +- .../[surveyId]/components/MediaBackground.tsx | 87 ++++++++++++ .../app/s/[surveyId]/components/PinScreen.tsx | 14 +- apps/web/app/s/[surveyId]/layout.tsx | 9 +- apps/web/app/s/[surveyId]/page.tsx | 34 +++-- packages/database/jsonTypes.ts | 2 + .../20231204180155_add_styling/migration.sql | 2 + packages/database/schema.prisma | 3 + packages/database/zod-utils.ts | 1 + packages/lib/constants.ts | 28 ++++ packages/lib/survey/service.ts | 2 + .../src/components/general/Headline.tsx | 5 +- .../surveys/src/components/general/Survey.tsx | 2 +- packages/types/surveys.ts | 19 +++ 27 files changed, 625 insertions(+), 82 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ImageSurveyBg.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx create mode 100644 apps/web/app/s/[surveyId]/components/MediaBackground.tsx create mode 100644 packages/database/migrations/20231204180155_add_styling/migration.sql diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index 2eefaf2f31..4846bc1ddf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -204,6 +204,7 @@ export async function copyToOtherEnvironmentAction( singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull, productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull, verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull, + styling: existingSurvey.styling ?? prismaClient.JsonNull, }, }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx new file mode 100644 index 0000000000..4e32eaeece --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx @@ -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?.styling?.background?.bg || "#ffff"); + const [hoveredVideo, setHoveredVideo] = useState(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 ( +
+
+ {Object.keys(animationFiles).map((key, index) => { + const value = animationFiles[key]; + return ( +
handleMouseEnter(index)} + onMouseLeave={() => handleMouseLeave(index)} + onClick={() => handleBg(value)} + className="relative cursor-pointer overflow-hidden rounded-lg"> + + handleBg(value)} + /> +
+ ); + })} +
+
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx new file mode 100644 index 0000000000..0362b9f5a0 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ColorSurveyBg.tsx @@ -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?.styling?.background?.bg || "#ffff"); + + const handleBg = (x: string) => { + setColor(x); + handleBgChange(x, "color"); + }; + return ( +
+
+ +
+
+ {colours.map((x) => { + return ( +
handleBg(x)}>
+ ); + })} +
+
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ImageSurveyBg.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ImageSurveyBg.tsx new file mode 100644 index 0000000000..3627268dfe --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ImageSurveyBg.tsx @@ -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?.styling?.background?.bg ?? "") + ? localSurvey?.styling?.background?.bg ?? "" + : ""; + + return ( +
+
+ { + if (url.length > 0) { + handleBgChange(url[0], "image"); + } else { + handleBgChange("#ffff", "color"); + } + }} + fileUrl={fileUrl} + /> +
+
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx index 55050535ae..e5f81b57bd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx @@ -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 (
@@ -60,7 +62,7 @@ export default function SettingsView({ environmentId={environment.id} /> - +
); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingCard.tsx index 4da6f0f244..251825cfd4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingCard.tsx @@ -1,7 +1,7 @@ "use client"; import { TPlacement } from "@formbricks/types/common"; -import { TSurvey } from "@formbricks/types/surveys"; +import { TSurvey, TSurveyBackgroundBgType } from "@formbricks/types/surveys"; import { ColorPicker } from "@formbricks/ui/ColorPicker"; import { Label } from "@formbricks/ui/Label"; import { Switch } from "@formbricks/ui/Switch"; @@ -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>; + 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, styling } = localSurvey; const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {}; + const { bg, bgType, brightness } = styling?.background ?? {}; + + const [inputValue, setInputValue] = useState(100); + + const handleInputChange = (e) => { + setInputValue(e.target.value); + handleBrightnessChange(parseInt(e.target.value)); + }; const togglePlacement = () => { setLocalSurvey({ @@ -42,6 +52,34 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard }); }; + const toggleBackgroundColor = () => { + setLocalSurvey({ + ...localSurvey, + styling: { + ...localSurvey.styling, + background: { + ...localSurvey.styling?.background, + bg: !!bg ? undefined : "#ffff", + bgType: !!bg ? undefined : "color", + }, + }, + }); + }; + + const toggleBrightness = () => { + setLocalSurvey({ + ...localSurvey, + styling: { + ...localSurvey.styling, + background: { + ...localSurvey.styling?.background, + brightness: !!brightness ? undefined : 100, + }, + }, + }); + setInputValue(100); + }; + const toggleHighlightBorderColor = () => { setLocalSurvey({ ...localSurvey, @@ -62,6 +100,35 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard }); }; + const handleBgChange = (color: string, type: TSurveyBackgroundBgType) => { + setInputValue(100); + setLocalSurvey({ + ...localSurvey, + styling: { + ...localSurvey.styling, + background: { + ...localSurvey.styling?.background, + bg: color, + bgType: type, + brightness: undefined, + }, + }, + }); + }; + + const handleBrightnessChange = (percent: number) => { + setLocalSurvey({ + ...localSurvey, + styling: { + ...(localSurvey.styling || {}), + background: { + ...localSurvey.styling?.background, + brightness: percent, + }, + }, + }); + }; + const handleBorderColorChange = (color: string) => { setLocalSurvey({ ...localSurvey, @@ -143,6 +210,66 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard )} + {type == "link" && ( + <> + {/* Background */} +
+
+ + +
+ {bg && ( + + )} +
+ {/* Overlay */} +
+
+ + +
+ {brightness && ( +
+
+

Transparency

+ +
+
+ )} +
+ + )} {/* positioning */} {type !== "link" && (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx new file mode 100644 index 0000000000..17c41bac3d --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyBgSelectorTab.tsx @@ -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 }) => ( + +); + +export default function SurveyBgSelectorTab({ + localSurvey, + handleBgChange, + colours, + bgType, +}: SurveyBgSelectorTabProps) { + const [tab, setTab] = useState(bgType || "image"); + + const renderContent = () => { + switch (tab) { + case "image": + return ; + case "animation": + return ; + case "color": + return ; + default: + return null; + } + }; + + return ( +
+
+ setTab("image")}> + Image + + setTab("animation")}> + Animation + + setTab("color")}> + Color + +
+ {renderContent()} +
+ ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx index 1aa9beb1bb..55c6a3e063 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx @@ -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(null); @@ -99,6 +101,7 @@ export default function SurveyEditor({ attributeClasses={attributeClasses} responseCount={responseCount} membershipRole={membershipRole} + colours={colours} /> )} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx index 8a98013ee5..fb06a8c881 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx @@ -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} /> ); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx index cfc706faa4..b5610c4d38 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx @@ -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 )}> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx index c22dfb45ea..baa38f9c8f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx @@ -2,7 +2,7 @@ import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal"; import TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption"; - +import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground"; import type { TEnvironment } from "@formbricks/types/environment"; import type { TProduct } from "@formbricks/types/product"; import { TUploadFileConfig } from "@formbricks/types/storage"; @@ -155,6 +155,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.styling?.background?.bgType === "animation") { + animationTrigger(); + } + }, [survey.styling?.background?.bg]); + useEffect(() => { if (environment && environment.widgetSetupCompleted) { setWidgetSetupCompleted(true); @@ -194,7 +208,7 @@ export default function PreviewSurvey({
-
+ {/* below element is use to create notch for the mobile device mockup */}
{previewType === "modal" ? ( @@ -214,25 +228,19 @@ export default function PreviewSurvey({ /> ) : ( -
-
-
- -
-
+
+
)} -
+
)} {previewMode === "desktop" && ( @@ -287,22 +295,20 @@ export default function PreviewSurvey({ /> ) : ( -
-
-
- -
+ +
+
-
+ )}
)} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts index 272826db12..975c3f1fb2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts @@ -2526,4 +2526,5 @@ export const minimalSurvey: TSurvey = { }, productOverwrites: null, singleUse: null, + styling: null, }; diff --git a/apps/web/app/s/[surveyId]/components/LegalFooter.tsx b/apps/web/app/s/[surveyId]/components/LegalFooter.tsx index dbe33b4815..a00164ba36 100644 --- a/apps/web/app/s/[surveyId]/components/LegalFooter.tsx +++ b/apps/web/app/s/[surveyId]/components/LegalFooter.tsx @@ -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 ( -
-
+
+
{IMPRINT_URL && ( - + Imprint )} - {IMPRINT_URL && PRIVACY_URL && | } + {IMPRINT_URL && PRIVACY_URL && |} {PRIVACY_URL && ( - + Privacy Policy )} diff --git a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx index 2a2288e973..d63e33c6e9 100644 --- a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx @@ -1,20 +1,20 @@ "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 { TUploadFileConfig } from "@formbricks/types/storage"; 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"; -import { TUploadFileConfig } from "@formbricks/types/storage"; interface LinkSurveyProps { survey: TSurvey; @@ -119,7 +119,7 @@ export default function LinkSurvey({ return ( <> - + {isPreview && (
diff --git a/apps/web/app/s/[surveyId]/components/MediaBackground.tsx b/apps/web/app/s/[surveyId]/components/MediaBackground.tsx new file mode 100644 index 0000000000..d5e703d4a6 --- /dev/null +++ b/apps/web/app/s/[surveyId]/components/MediaBackground.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { TSurvey } from "@formbricks/types/surveys"; +import React from "react"; + +interface MediaBackgroundProps { + children: React.ReactNode; + survey: TSurvey; + isEditorView?: boolean; + isMobilePreview?: boolean; + ContentRef?: React.RefObject; +} + +export const MediaBackground: React.FC = ({ + children, + survey, + isEditorView = false, + isMobilePreview = false, + ContentRef, +}) => { + const getFilterStyle = () => { + return survey.styling?.background?.brightness + ? `brightness-${survey.styling?.background?.brightness}` + : ""; + }; + + const renderBackground = () => { + const filterStyle = getFilterStyle(); + const baseClasses = "absolute inset-0 h-full w-full"; + + switch (survey.styling?.background?.bgType) { + case "color": + return ( +
+ ); + case "animation": + return ( + + ); + case "image": + return ( +
+ ); + default: + return
; + } + }; + + const renderContent = () => ( +
{children}
+ ); + + if (isMobilePreview) { + return ( +
+ {renderBackground()} + {renderContent()} +
+ ); + } else if (isEditorView) { + return ( +
+
+ {renderBackground()} +
{children}
+
+
+ ); + } else { + return ( +
+ {renderBackground()} +
{children}
+
+ ); + } +}; diff --git a/apps/web/app/s/[surveyId]/components/PinScreen.tsx b/apps/web/app/s/[surveyId]/components/PinScreen.tsx index 529984ffe9..9f1c3975b0 100644 --- a/apps/web/app/s/[surveyId]/components/PinScreen.tsx +++ b/apps/web/app/s/[surveyId]/components/PinScreen.tsx @@ -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; diff --git a/apps/web/app/s/[surveyId]/layout.tsx b/apps/web/app/s/[surveyId]/layout.tsx index 9c17d7acd4..65b2729037 100644 --- a/apps/web/app/s/[surveyId]/layout.tsx +++ b/apps/web/app/s/[surveyId]/layout.tsx @@ -1,10 +1,3 @@ -import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter"; - export default async function SurveyLayout({ children }) { - return ( -
-
{children}
- -
- ); + return
{children}
; } diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx index 6c9fb08e3e..2820c62640 100644 --- a/apps/web/app/s/[surveyId]/page.tsx +++ b/apps/web/app/s/[surveyId]/page.tsx @@ -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"; import { ZId } from "@formbricks/types/environment"; @@ -183,17 +186,22 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve ); } - return ( - - ); + return survey ? ( +
+ + + + +
+ ) : null; } diff --git a/packages/database/jsonTypes.ts b/packages/database/jsonTypes.ts index 73432b42e7..8560d63514 100644 --- a/packages/database/jsonTypes.ts +++ b/packages/database/jsonTypes.ts @@ -6,6 +6,7 @@ import { TSurveyClosedMessage, TSurveyHiddenFields, TSurveyProductOverwrites, + TSurveyStyling, TSurveyQuestions, TSurveySingleUse, TSurveyThankYouCard, @@ -27,6 +28,7 @@ declare global { export type SurveyThankYouCard = TSurveyThankYouCard; export type SurveyHiddenFields = TSurveyHiddenFields; export type SurveyProductOverwrites = TSurveyProductOverwrites; + export type SurveyStyling = TSurveyStyling; export type SurveyClosedMessage = TSurveyClosedMessage; export type SurveySingleUse = TSurveySingleUse; export type SurveyVerifyEmail = TSurveyVerifyEmail; diff --git a/packages/database/migrations/20231204180155_add_styling/migration.sql b/packages/database/migrations/20231204180155_add_styling/migration.sql new file mode 100644 index 0000000000..53f0b691d7 --- /dev/null +++ b/packages/database/migrations/20231204180155_add_styling/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Survey" ADD COLUMN "styling" JSONB; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index f5a90c9b81..a7a1099da2 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -289,6 +289,9 @@ model Survey { /// @zod.custom(imports.ZSurveyProductOverwrites) /// [SurveyProductOverwrites] productOverwrites Json? + /// @zod.custom(imports.ZSurveyStyling) + /// [SurveyStyling] + styling Json? /// @zod.custom(imports.ZSurveySingleUse) /// [SurveySingleUse] singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}") diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts index 61cdddb386..65ae087715 100644 --- a/packages/database/zod-utils.ts +++ b/packages/database/zod-utils.ts @@ -18,6 +18,7 @@ export { ZSurveyHiddenFields, ZSurveyClosedMessage, ZSurveyProductOverwrites, + ZSurveyStyling, ZSurveyVerifyEmail, ZSurveySingleUse, } from "@formbricks/types/surveys"; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index b3f569a666..d0cb6cf124 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -71,6 +71,34 @@ export const IS_S3_CONFIGURED: boolean = 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 diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index 2477d8d4e6..5a1d9a590d 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -45,6 +45,7 @@ export const selectSurvey = { verifyEmail: true, redirectUrl: true, productOverwrites: true, + styling: true, surveyClosedMessage: true, singleUse: true, pin: true, @@ -587,6 +588,7 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) = productOverwrites: existingSurvey.productOverwrites ? JSON.parse(JSON.stringify(existingSurvey.productOverwrites)) : Prisma.JsonNull, + styling: existingSurvey.styling ? JSON.parse(JSON.stringify(existingSurvey.styling)) : Prisma.JsonNull, verifyEmail: existingSurvey.verifyEmail ? JSON.parse(JSON.stringify(existingSurvey.verifyEmail)) : Prisma.JsonNull, diff --git a/packages/surveys/src/components/general/Headline.tsx b/packages/surveys/src/components/general/Headline.tsx index 80201b487c..19742f3012 100644 --- a/packages/surveys/src/components/general/Headline.tsx +++ b/packages/surveys/src/components/general/Headline.tsx @@ -13,11 +13,10 @@ export default function Headline({ }: HeadlineProps) { return (