diff --git a/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx b/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx index be4b00496b..b734fb6b6b 100644 --- a/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx +++ b/apps/web/app/s/[surveyId]/components/LinkSurveyWrapper.tsx @@ -1,4 +1,5 @@ import { LegalFooter } from "@/app/s/[surveyId]/components/LegalFooter"; +import { SurveyLoadingAnimation } from "@/app/s/[surveyId]/components/SurveyLoadingAnimation"; import React from "react"; import { cn } from "@formbricks/lib/cn"; import { TProduct, TProductStyling } from "@formbricks/types/product"; @@ -44,12 +45,14 @@ export const LinkSurveyWrapper = ({ styling.cardArrangement?.linkSurveys === "straight" && "pt-6", styling.cardArrangement?.linkSurveys === "casual" && "px-6 py-10" )}> + {children} ); else return (
+
{!styling.isLogoHidden && product.logo?.url && } diff --git a/apps/web/app/s/[surveyId]/components/SurveyLoadingAnimation.tsx b/apps/web/app/s/[surveyId]/components/SurveyLoadingAnimation.tsx new file mode 100644 index 0000000000..8bfa414bae --- /dev/null +++ b/apps/web/app/s/[surveyId]/components/SurveyLoadingAnimation.tsx @@ -0,0 +1,112 @@ +import Logo from "@/images/powered-by-formbricks.svg"; +import Image from "next/image"; +import { useCallback, useEffect, useState } from "react"; +import { cn } from "@formbricks/lib/cn"; +import { TSurvey } from "@formbricks/types/surveys/types"; +import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner"; + +interface SurveyLoadingAnimationProps { + survey: TSurvey; +} + +export const SurveyLoadingAnimation = ({ survey }: SurveyLoadingAnimationProps) => { + const [isHidden, setIsHidden] = useState(false); + const [minTimePassed, setMinTimePassed] = useState(false); + const [isMediaLoaded, setIsMediaLoaded] = useState(false); // Tracks if all media (images, iframes) are fully loaded + const [isSurveyPackageLoaded, setIsSurveyPackageLoaded] = useState(false); // Tracks if the survey package has been loaded into the DOM + + const cardId = survey.welcomeCard.enabled ? `questionCard--1` : `questionCard-0`; + + // Function to check if all media elements (images and iframes) within the survey card are loaded + const checkMediaLoaded = useCallback(() => { + const cardElement = document.getElementById(cardId); + const images = cardElement ? Array.from(cardElement.getElementsByTagName("img")) : []; + const iframes = cardElement ? Array.from(cardElement.getElementsByTagName("iframe")) : []; + + const allImagesLoaded = images.every((img) => img.complete && img.naturalHeight !== 0); + const allIframesLoaded = iframes.every((iframe) => { + const contentWindow = iframe.contentWindow; + return contentWindow && contentWindow.document.readyState === "complete"; + }); + + if (allImagesLoaded && allIframesLoaded) { + setIsMediaLoaded(true); + } + }, [cardId]); + + // Effect to monitor when the survey package is loaded and media elements are fully loaded + useEffect(() => { + if (!isSurveyPackageLoaded) return; // Exit early if the survey package is not yet loaded + + checkMediaLoaded(); // Initial check when the survey package is loaded + + // Add event listeners to detect when individual media elements finish loading + const mediaElements = document.querySelectorAll(`#${cardId} img, #${cardId} iframe`); + mediaElements.forEach((element) => element.addEventListener("load", checkMediaLoaded)); + + return () => { + // Cleanup event listeners when the component unmounts or dependencies change + mediaElements.forEach((element) => element.removeEventListener("load", checkMediaLoaded)); + }; + }, [isSurveyPackageLoaded, checkMediaLoaded, cardId]); + + // Effect to handle the hiding of the animation once both media are loaded and the minimum time has passed + useEffect(() => { + if (isMediaLoaded && minTimePassed) { + const hideTimer = setTimeout(() => { + setIsHidden(true); + }, 1500); + + return () => clearTimeout(hideTimer); + } else { + setIsHidden(false); + } + }, [isMediaLoaded, minTimePassed]); + + useEffect(() => { + // Ensure the animation is shown for at least 1.5 seconds + const minTimeTimer = setTimeout(() => { + setMinTimePassed(true); + }, 1500); + + // Observe the DOM for when the survey package (child elements) is added to the target node + const observer = new MutationObserver((mutations) => { + mutations.some((mutation) => { + if (mutation.addedNodes.length) { + setIsSurveyPackageLoaded(true); + observer.disconnect(); + return true; + } + return false; + }); + }); + + const targetNode = document.getElementById("formbricks-survey-container"); + if (targetNode) { + observer.observe(targetNode, { childList: true }); + } + + return () => { + observer.disconnect(); + clearTimeout(minTimeTimer); + }; + }, []); + + return ( +
+
+ Logo + +
+
+ ); +}; diff --git a/apps/web/images/logo.svg b/apps/web/images/logo.svg deleted file mode 100644 index f3795d5c77..0000000000 --- a/apps/web/images/logo.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/images/powered-by-formbricks.svg b/apps/web/images/powered-by-formbricks.svg new file mode 100644 index 0000000000..6ba6fe7408 --- /dev/null +++ b/apps/web/images/powered-by-formbricks.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index bbdbf017f5..7267d23d00 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -4,4 +4,22 @@ const base = require("../../packages/config-tailwind/tailwind.config"); module.exports = { ...base, content: [...base.content], + theme: { + extend: { + keyframes: { + surveyLoadingAnimation: { + "0%": { transform: "translateY(50px)", opacity: "0" }, + "100%": { transform: "translateY(0)", opacity: "1" }, + }, + surveyExitAnimation: { + "0%": { transform: "translateY(0)", opacity: "1" }, + "100%": { transform: "translateY(-50px)", opacity: "0" }, + }, + }, + animation: { + surveyLoading: "surveyLoadingAnimation 0.5s ease-out forwards", + surveyExit: "surveyExitAnimation 0.5s ease-out forwards", + }, + }, + }, }; diff --git a/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx b/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx index a6176a349b..a2cdcce730 100644 --- a/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx +++ b/packages/surveys/src/components/wrappers/StackedCardsContainer.tsx @@ -173,6 +173,7 @@ export const StackedCardsContainer = ({
{cardArrangement === "simple" ? (