diff --git a/apps/web/components/Shell.tsx b/apps/web/components/Shell.tsx
index aba57193..03914035 100644
--- a/apps/web/components/Shell.tsx
+++ b/apps/web/components/Shell.tsx
@@ -125,7 +125,7 @@ const Layout = ({
status,
plan,
...props
-}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan }) => {
+}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
const isEmbed = useIsEmbed();
const router = useRouter();
const { t } = useLocale();
@@ -342,7 +342,7 @@ const Layout = ({
"px-4 sm:px-6 md:px-8",
props.flexChildrenContainer && "flex flex-1 flex-col"
)}>
- {props.children}
+ {!props.isLoading ? props.children : props.customLoader}
{/* show bottom navigation for md and smaller (tablet and phones) */}
{status === "authenticated" && (
@@ -403,6 +403,7 @@ type LayoutProps = {
// use when content needs to expand with flex
flexChildrenContainer?: boolean;
isPublic?: boolean;
+ customLoader?: ReactNode;
};
export default function Shell(props: LayoutProps) {
@@ -423,8 +424,10 @@ export default function Shell(props: LayoutProps) {
const i18n = useViewerI18n();
const { status } = useSession();
- if (i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading) {
- // show spinner whilst i18n is loading to avoid language flicker
+ const isLoading =
+ i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading;
+
+ if (isLoading && !props.customLoader) {
return (
@@ -437,7 +440,7 @@ export default function Shell(props: LayoutProps) {
return (
<>
-
+
>
);
}
diff --git a/apps/web/components/apps/SkeletonLoader.tsx b/apps/web/components/apps/SkeletonLoader.tsx
new file mode 100644
index 00000000..8dc31def
--- /dev/null
+++ b/apps/web/components/apps/SkeletonLoader.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+
+import { ShellSubHeading } from "@components/Shell";
+
+function SkeletonLoader() {
+ return (
+ <>
+
} />
+
+ >
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/web/components/availability/SkeletonLoader.tsx b/apps/web/components/availability/SkeletonLoader.tsx
new file mode 100644
index 00000000..2ba945b8
--- /dev/null
+++ b/apps/web/components/availability/SkeletonLoader.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+function SkeletonLoader() {
+ return (
+
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/web/components/booking/SkeletonLoader.tsx b/apps/web/components/booking/SkeletonLoader.tsx
new file mode 100644
index 00000000..e152b4d1
--- /dev/null
+++ b/apps/web/components/booking/SkeletonLoader.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+import BookingsShell from "@components/BookingsShell";
+
+function SkeletonLoader() {
+ return (
+
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/web/components/eventtype/SkeletonLoader.tsx b/apps/web/components/eventtype/SkeletonLoader.tsx
new file mode 100644
index 00000000..ed77feb6
--- /dev/null
+++ b/apps/web/components/eventtype/SkeletonLoader.tsx
@@ -0,0 +1,52 @@
+import { LinkIcon } from "@heroicons/react/outline";
+import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon, UserIcon } from "@heroicons/react/solid";
+import React from "react";
+
+function SkeletonLoader() {
+ return (
+
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/QueryCell.tsx b/apps/web/lib/QueryCell.tsx
index 79a7d7db..5c0805ea 100644
--- a/apps/web/lib/QueryCell.tsx
+++ b/apps/web/lib/QueryCell.tsx
@@ -1,3 +1,4 @@
+import React, { ReactNode } from "react";
import {
QueryObserverIdleResult,
QueryObserverLoadingErrorResult,
@@ -31,6 +32,7 @@ type JSXElementOrNull = JSX.Element | null;
interface QueryCellOptionsBase {
query: UseQueryResult;
+ customLoader?: ReactNode;
error?: (
query: QueryObserverLoadingErrorResult | QueryObserverRefetchErrorResult
) => JSXElementOrNull;
@@ -77,10 +79,10 @@ export function QueryCell(
);
}
if (query.status === "loading") {
- return opts.loading?.(query) ?? ;
+ return opts.loading?.(query) ?? opts.customLoader ? opts.customLoader : ;
}
if (query.status === "idle") {
- return opts.idle?.(query) ?? ;
+ return opts.idle?.(query) ?? opts.customLoader ? opts.customLoader : ;
}
// impossible state
return null;
@@ -108,6 +110,7 @@ const withQuery = (
>
) {
const query = trpc.useQuery(pathAndInput, params);
+
return ;
};
};
diff --git a/apps/web/pages/apps/installed.tsx b/apps/web/pages/apps/installed.tsx
index b346681f..fd2cf564 100644
--- a/apps/web/pages/apps/installed.tsx
+++ b/apps/web/pages/apps/installed.tsx
@@ -18,8 +18,8 @@ import { trpc } from "@lib/trpc";
import AppsShell from "@components/AppsShell";
import { ClientSuspense } from "@components/ClientSuspense";
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
-import Loader from "@components/Loader";
import Shell, { ShellSubHeading } from "@components/Shell";
+import SkeletonLoader from "@components/apps/SkeletonLoader";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
import IntegrationListItem from "@components/integrations/IntegrationListItem";
@@ -332,9 +332,13 @@ export default function IntegrationsPage() {
const { t } = useLocale();
return (
-
+ }>
- }>
+ }>
diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx
index 01880eae..6de76507 100644
--- a/apps/web/pages/availability/index.tsx
+++ b/apps/web/pages/availability/index.tsx
@@ -16,6 +16,7 @@ import { inferQueryOutput, trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Shell from "@components/Shell";
import { NewScheduleButton } from "@components/availability/NewScheduleButton";
+import SkeletonLoader from "@components/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
const { t, i18n } = useLocale();
@@ -105,8 +106,12 @@ export default function AvailabilityPage() {
const { t } = useLocale();
return (
-
}>
-
} />
+ }
+ customLoader={}>
+ } customLoader={} />
);
diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx
index f622f2b8..b804c58b 100644
--- a/apps/web/pages/bookings/[status].tsx
+++ b/apps/web/pages/bookings/[status].tsx
@@ -12,9 +12,9 @@ import { inferQueryInput, trpc } from "@lib/trpc";
import BookingsShell from "@components/BookingsShell";
import EmptyScreen from "@components/EmptyScreen";
-import Loader from "@components/Loader";
import Shell from "@components/Shell";
import BookingListItem from "@components/booking/BookingListItem";
+import SkeletonLoader from "@components/booking/SkeletonLoader";
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
@@ -45,7 +45,10 @@ export default function Bookings() {
const isEmpty = !query.data?.pages[0]?.bookings.length;
return (
-
+ }>
@@ -54,7 +57,7 @@ export default function Bookings() {
{query.status === "error" && (
)}
- {(query.status === "loading" || query.status === "idle") &&
}
+ {(query.status === "loading" || query.status === "idle") &&
}
{query.status === "success" && !isEmpty && (
<>
diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx
index 86697910..75c366f5 100644
--- a/apps/web/pages/event-types/index.tsx
+++ b/apps/web/pages/event-types/index.tsx
@@ -41,6 +41,7 @@ import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
+import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import Badge from "@components/ui/Badge";
@@ -523,8 +524,13 @@ const EventTypesPage = () => {
Home | Cal.com
- }>
+ }
+ customLoader={}>
}
success={({ data }) => (
<>
{data.viewer.plan === "FREE" && !data.viewer.canAddEvents && (