mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-24 18:39:10 -06:00
fix: Loading Skeleton for survey summary page (#3108)
Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
This commit is contained in:
@@ -6,20 +6,25 @@ interface SummaryMetadataProps {
|
||||
setShowDropOffs: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showDropOffs: boolean;
|
||||
surveySummary: TSurveySummary["meta"];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const StatCard = ({ label, percentage, value, tooltipText }) => (
|
||||
const StatCard = ({ label, percentage, value, tooltipText, isLoading }) => (
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-xl border border-slate-200 bg-white p-4 text-left shadow-sm">
|
||||
<p className="flex items-center gap-1 text-sm text-slate-600">
|
||||
{label}
|
||||
{typeof percentage === "number" && !isNaN(percentage) && (
|
||||
{typeof percentage === "number" && !isNaN(percentage) && !isLoading && (
|
||||
<span className="ml-1 rounded-xl bg-slate-100 px-2 py-1 text-xs">{percentage}%</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-slate-800">{value}</p>
|
||||
{isLoading ? (
|
||||
<div className="h-6 w-12 animate-pulse rounded-full bg-slate-200"></div>
|
||||
) : (
|
||||
<p className="text-2xl font-bold text-slate-800">{value}</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -44,7 +49,12 @@ const formatTime = (ttc) => {
|
||||
return formattedValue;
|
||||
};
|
||||
|
||||
export const SummaryMetadata = ({ setShowDropOffs, showDropOffs, surveySummary }: SummaryMetadataProps) => {
|
||||
export const SummaryMetadata = ({
|
||||
setShowDropOffs,
|
||||
showDropOffs,
|
||||
surveySummary,
|
||||
isLoading,
|
||||
}: SummaryMetadataProps) => {
|
||||
const {
|
||||
completedPercentage,
|
||||
completedResponses,
|
||||
@@ -64,18 +74,21 @@ export const SummaryMetadata = ({ setShowDropOffs, showDropOffs, surveySummary }
|
||||
percentage={null}
|
||||
value={displayCount === 0 ? <span>-</span> : displayCount}
|
||||
tooltipText="Number of times the survey has been viewed."
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatCard
|
||||
label="Starts"
|
||||
percentage={Math.round(startsPercentage) > 100 ? null : Math.round(startsPercentage)}
|
||||
value={totalResponses === 0 ? <span>-</span> : totalResponses}
|
||||
tooltipText="Number of times the survey has been started."
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatCard
|
||||
label="Completed"
|
||||
percentage={Math.round(completedPercentage) > 100 ? null : Math.round(completedPercentage)}
|
||||
value={completedResponses === 0 ? <span>-</span> : completedResponses}
|
||||
tooltipText="Number of times the survey has been completed."
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<TooltipProvider delayDuration={50}>
|
||||
@@ -86,21 +99,29 @@ export const SummaryMetadata = ({ setShowDropOffs, showDropOffs, surveySummary }
|
||||
className="group flex h-full w-full cursor-pointer flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
|
||||
<span className="text-sm text-slate-600">
|
||||
Drop-Offs
|
||||
{`${Math.round(dropOffPercentage)}%` !== "NaN%" && (
|
||||
{`${Math.round(dropOffPercentage)}%` !== "NaN%" && !isLoading && (
|
||||
<span className="ml-1 rounded-xl bg-slate-100 px-2 py-1 text-xs">{`${Math.round(dropOffPercentage)}%`}</span>
|
||||
)}
|
||||
</span>
|
||||
<div className="flex w-full items-end justify-between">
|
||||
<span className="text-2xl font-bold text-slate-800">
|
||||
{dropOffCount === 0 ? <span>-</span> : dropOffCount}
|
||||
</span>
|
||||
<span className="ml-1 flex items-center rounded-md bg-slate-800 px-2 py-1 text-xs text-slate-50 group-hover:bg-slate-700">
|
||||
{showDropOffs ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
{isLoading ? (
|
||||
<div className="h-6 w-12 animate-pulse rounded-full bg-slate-200"></div>
|
||||
) : dropOffCount === 0 ? (
|
||||
<span>-</span>
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
dropOffCount
|
||||
)}
|
||||
</span>
|
||||
{!isLoading && (
|
||||
<span className="ml-1 flex items-center rounded-md bg-slate-800 px-2 py-1 text-xs text-slate-50 group-hover:bg-slate-700">
|
||||
{showDropOffs ? (
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
@@ -114,6 +135,7 @@ export const SummaryMetadata = ({ setShowDropOffs, showDropOffs, surveySummary }
|
||||
percentage={null}
|
||||
value={ttcAverage === 0 ? <span>-</span> : `${formatTime(ttcAverage)}`}
|
||||
tooltipText="Average time to complete the survey."
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +67,7 @@ export const SummaryPage = ({
|
||||
const [responseCount, setResponseCount] = useState<number | null>(null);
|
||||
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
|
||||
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
|
||||
@@ -104,28 +105,39 @@ export const SummaryPage = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleInitialData = async () => {
|
||||
const handleInitialData = async (isInitialLoad = false) => {
|
||||
if (isInitialLoad) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedResponseCountData = await getResponseCount();
|
||||
const updatedSurveySummary = await getSummary();
|
||||
const [updatedResponseCountData, updatedSurveySummary] = await Promise.all([
|
||||
getResponseCount(),
|
||||
getSummary(),
|
||||
]);
|
||||
|
||||
const responseCount = updatedResponseCountData?.data ?? 0;
|
||||
const surveySummary = updatedSurveySummary?.data ?? initialSurveySummary;
|
||||
|
||||
// Update the state with new data
|
||||
setResponseCount(responseCount);
|
||||
setSurveySummary(surveySummary);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
if (isInitialLoad) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleInitialData();
|
||||
handleInitialData(true);
|
||||
}, [JSON.stringify(filters), isSharingPage, sharingKey, surveyId]);
|
||||
|
||||
useIntervalWhenFocused(
|
||||
() => {
|
||||
handleInitialData();
|
||||
handleInitialData(false);
|
||||
},
|
||||
10000,
|
||||
!isShareEmbedModalOpen,
|
||||
@@ -148,6 +160,7 @@ export const SummaryPage = ({
|
||||
surveySummary={surveySummary.meta}
|
||||
showDropOffs={showDropOffs}
|
||||
setShowDropOffs={setShowDropOffs}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{showDropOffs && <SummaryDropOffs dropOff={surveySummary.dropOff} />}
|
||||
<div className="flex gap-1.5">
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
import { SkeletonLoader } from "@formbricks/ui/SkeletonLoader";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Summary" />
|
||||
<div className="flex h-9 animate-pulse gap-2">
|
||||
<div className="h-9 w-36 rounded-full bg-slate-200"></div>
|
||||
<div className="h-9 w-36 rounded-full bg-slate-200"></div>
|
||||
<div className="h-9 w-36 rounded-full bg-slate-200"></div>
|
||||
<div className="h-9 w-36 rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<SkeletonLoader type="summary" />
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
@@ -11,16 +11,14 @@ export const SkeletonLoader = ({ type }: SkeletonLoaderProps) => {
|
||||
className="rounded-xl border border-slate-200 bg-white shadow-sm"
|
||||
data-testid="skeleton-loader-summary">
|
||||
<Skeleton className="group space-y-4 rounded-xl bg-white p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="h-6 w-full rounded-full bg-slate-100"></div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4">
|
||||
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-24 rounded-full bg-slate-100"></div>
|
||||
<div className="h-6 w-24 rounded-full bg-slate-200"></div>
|
||||
<div className="h-6 w-24 rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></div>
|
||||
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
|
||||
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-200 text-sm text-slate-500"></div>
|
||||
<div className="h-12 w-full rounded-full bg-slate-200"></div>
|
||||
<div className="h-12 w-full rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
@@ -31,13 +29,13 @@ export const SkeletonLoader = ({ type }: SkeletonLoaderProps) => {
|
||||
return (
|
||||
<div className="group space-y-4 rounded-lg bg-white p-6" data-testid="skeleton-loader-response">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></Skeleton>
|
||||
<Skeleton className="h-6 w-full rounded-full bg-slate-100"></Skeleton>
|
||||
<Skeleton className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-200"></Skeleton>
|
||||
<Skeleton className="h-6 w-full rounded-full bg-slate-200"></Skeleton>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-12 w-full rounded-full bg-slate-100"></Skeleton>
|
||||
<Skeleton className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100"></Skeleton>
|
||||
<Skeleton className="h-12 w-full rounded-full bg-slate-50/50"></Skeleton>
|
||||
<Skeleton className="h-12 w-full rounded-full bg-slate-200"></Skeleton>
|
||||
<Skeleton className="flex h-12 w-full items-center justify-center rounded-full bg-slate-200 text-sm text-slate-500"></Skeleton>
|
||||
<Skeleton className="h-12 w-full rounded-full bg-slate-200"></Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user