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 && (