feat: Loading indicator for link surveys (#2982)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Dhruwang Jariwala
2024-08-14 22:04:02 +05:30
committed by GitHub
parent e0208da0ac
commit 49cd06a9b4
6 changed files with 138 additions and 73 deletions

View File

@@ -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"
)}>
<SurveyLoadingAnimation survey={survey} />
{children}
</div>
);
else
return (
<div>
<SurveyLoadingAnimation survey={survey} />
<MediaBackground survey={survey} product={product}>
<div className="flex max-h-dvh min-h-dvh items-end justify-center overflow-clip md:items-center">
{!styling.isLogoHidden && product.logo?.url && <ClientLogo product={product} />}

View File

@@ -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 (
<div
className={cn(
"absolute inset-0 z-[5000] flex items-center justify-center transition-colors duration-1000",
isMediaLoaded && minTimePassed ? "bg-transparent" : "bg-white",
isHidden && "hidden"
)}>
<div
className={cn(
"flex flex-col items-center space-y-4",
isMediaLoaded && minTimePassed ? "animate-surveyExit" : "animate-surveyLoading"
)}>
<Image src={Logo} alt="Logo" className={cn("w-32 transition-all duration-1000 md:w-40")} />
<LoadingSpinner />
</div>
</div>
);
};

View File

@@ -1,73 +0,0 @@
<svg width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M39.1602 147.334H95.8321V175.67C95.8321 191.32 83.1457 204.006 67.4962 204.006C51.8466 204.006 39.1602 191.32 39.1602 175.67V147.334Z" fill="url(#paint0_linear_415_2)"/>
<path d="M39.1602 81.8071H152.504C168.154 81.8071 180.84 94.4936 180.84 110.143C180.84 125.793 168.154 138.479 152.504 138.479H39.1602V81.8071Z" fill="url(#paint1_linear_415_2)"/>
<path d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z" fill="url(#paint2_linear_415_2)"/>
<mask id="mask0_415_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="39" y="16" width="142" height="189">
<path d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z" fill="url(#paint3_linear_415_2)"/>
<path d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z" fill="url(#paint4_linear_415_2)"/>
<path d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z" fill="url(#paint5_linear_415_2)"/>
</mask>
<g mask="url(#mask0_415_2)">
<g filter="url(#filter0_d_415_2)">
<mask id="mask1_415_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="39" y="16" width="142" height="189">
<path d="M39.1602 147.335H95.8321V175.671C95.8321 191.32 83.1457 204.007 67.4962 204.007C51.8466 204.007 39.1602 191.32 39.1602 175.671V147.335Z" fill="black" fill-opacity="0.1"/>
<path d="M39.1602 62.7322C39.1602 37.0773 59.9576 16.2798 85.6126 16.2798H152.504C168.154 16.2798 180.84 28.9662 180.84 44.6158C180.84 60.2653 168.154 72.9518 152.504 72.9518H39.1602V62.7322Z" fill="black" fill-opacity="0.1"/>
<path d="M39.1602 81.8081H152.504C168.154 81.8081 180.84 94.4946 180.84 110.144C180.84 125.794 168.154 138.48 152.504 138.48H39.1602V81.8081Z" fill="black" fill-opacity="0.1"/>
</mask>
<g mask="url(#mask1_415_2)">
<path d="M42.1331 -32.5321C64.3329 -54.1986 120.626 -32.5321 120.626 -32.5321H42.1331C36.6806 -27.2105 33.2847 -19.2749 33.2847 -7.76218C33.2847 50.6243 96.5317 71.8561 96.5317 112.55C96.5317 152.386 35.9231 176.962 33.3678 231.092H120.626C120.626 231.092 33.2847 291.248 33.2847 234.631C33.2847 233.437 33.3128 232.258 33.3678 231.092H-5.11523L2.41417 -32.5321H42.1331Z" fill="black" fill-opacity="0.1"/>
</g>
</g>
<g filter="url(#filter1_f_415_2)">
<circle cx="21.4498" cy="179.212" r="53.13" fill="#00C4B8"/>
</g>
<g filter="url(#filter2_f_415_2)">
<circle cx="21.4498" cy="44.6163" r="53.13" fill="#00C4B8"/>
</g>
</g>
<defs>
<filter id="filter0_d_415_2" x="34.5149" y="-11.5917" width="137.209" height="243.47" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="23.2262"/>
<feGaussianBlur stdDeviation="13.9357"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_415_2"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_415_2" result="shape"/>
</filter>
<filter id="filter1_f_415_2" x="-78.1326" y="79.6296" width="199.165" height="199.165" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2"/>
</filter>
<filter id="filter2_f_415_2" x="-78.1326" y="-54.9661" width="199.165" height="199.165" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="23.2262" result="effect1_foregroundBlur_415_2"/>
</filter>
<linearGradient id="paint0_linear_415_2" x1="96.0786" y1="174.643" x2="39.1553" y2="174.873" gradientUnits="userSpaceOnUse">
<stop offset="1" stop-color="#00C4B8"/>
</linearGradient>
<linearGradient id="paint1_linear_415_2" x1="181.456" y1="109.116" x2="39.1602" y2="110.554" gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint2_linear_415_2" x1="181.456" y1="43.5891" x2="39.1602" y2="45.0264" gradientUnits="userSpaceOnUse">
<stop stop-color="#00DDD0"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint3_linear_415_2" x1="96.0786" y1="174.644" x2="39.1553" y2="174.874" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint4_linear_415_2" x1="181.456" y1="109.117" x2="39.1602" y2="110.555" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
<linearGradient id="paint5_linear_415_2" x1="181.456" y1="43.5891" x2="39.1602" y2="45.0264" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFE1"/>
<stop offset="1" stop-color="#01E0C6"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@@ -173,6 +173,7 @@ export const StackedCardsContainer = ({
<div style={{ height: cardHeight }}></div>
{cardArrangement === "simple" ? (
<div
id={`questionCard-${questionIdxTemp}`}
className={cn("fb-w-full", fullSizeCards ? "fb-h-full" : "")}
style={{
...borderStyles,