perf: SSR - /availablilty/[schedule] (#20829)

* ssr for schedule page

* improve skeleton

* improve skeleton

* improve skeleton
This commit is contained in:
Benny Joo
2025-04-23 11:31:01 -03:00
committed by GitHub
parent b32b31ccdc
commit a957c15062
5 changed files with 178 additions and 35 deletions
@@ -0,0 +1,7 @@
"use server";
import { revalidatePath } from "next/cache";
export async function revalidateSchedulePage(scheduleId: number) {
revalidatePath(`/availability/${scheduleId}`);
}
@@ -0,0 +1,5 @@
import { AvailabilityScheduleSkeleton } from "./skeleton";
export default function Loading() {
return <AvailabilityScheduleSkeleton />;
}
@@ -1,8 +1,12 @@
import { createRouterCaller } from "app/_trpc/context";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { notFound } from "next/navigation";
import { z } from "zod";
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
import { travelSchedulesRouter } from "@calcom/trpc/server/routers/viewer/travelSchedules/_router";
import { AvailabilitySettingsWebWrapper } from "~/availability/[schedule]/schedule-view";
const querySchema = z.object({
@@ -29,8 +33,25 @@ const Page = async ({ params }: PageProps) => {
if (!parsed.success) {
notFound();
}
const scheduleId = parsed.data.schedule;
return <AvailabilitySettingsWebWrapper />;
const [availabilityCaller, travelSchedulesCaller] = await Promise.all([
createRouterCaller(availabilityRouter),
createRouterCaller(travelSchedulesRouter),
]);
const [scheduleData, travelSchedulesData] = await Promise.all([
availabilityCaller.schedule.get({ scheduleId }),
travelSchedulesCaller.get(),
]);
if (!scheduleData) {
notFound();
}
return (
<AvailabilitySettingsWebWrapper scheduleData={scheduleData} travelSchedulesData={travelSchedulesData} />
);
};
export default Page;
@@ -0,0 +1,132 @@
"use client";
import Shell from "@calcom/features/shell/Shell";
import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/components/skeleton";
export function AvailabilityScheduleSkeleton() {
return (
<Shell withoutMain={true}>
<SkeletonContainer>
<div className="max-w-full">
{/* Header section */}
<div className="bg-default sticky top-0 z-10 mb-0 flex items-center py-2 md:mb-6 md:mt-0 lg:mb-9">
{/* Back button */}
<div className="flex items-center">
<SkeletonText className="h-6 w-6 rounded-md ltr:mr-2 rtl:ml-2" /> {/* Back button */}
</div>
{/* Title section */}
<header className="flex w-full max-w-full items-center truncate">
<div className="w-full truncate ltr:mr-4 rtl:ml-4 md:block">
<SkeletonText className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden h-6 truncate text-xl font-semibold tracking-wide md:block" />
{/* Title */}
<SkeletonText className="mt-1 hidden h-4 w-64 text-sm md:block" /> {/* Subtitle */}
</div>
{/* Action buttons */}
<div className="relative flex-shrink-0 md:relative md:bottom-auto md:right-auto">
{/* Desktop buttons - Set to Default, Delete, and Save */}
<div className="flex items-center justify-end">
{/* Set to Default - desktop only */}
<div className="hidden items-center rounded-md sm:flex">
<SkeletonText className="h-4 w-28" /> {/* "Set to Default" text */}
<div className="ml-2 h-5 w-11 rounded-full bg-gray-200" /> {/* Toggle */}
<div className="mx-3 h-4 w-px bg-gray-200" /> {/* Divider */}
</div>
{/* Delete button */}
<SkeletonButton className="mr-2 hidden h-9 w-9 rounded-md sm:inline-flex" />
{/* Second divider */}
<div className="mx-3 hidden h-4 w-px bg-gray-200 sm:inline" /> {/* Divider */}
{/* Save button - updated to match screenshot */}
<SkeletonButton className="h-9 w-20 rounded-md bg-gray-200" /> {/* Save button */}
</div>
</div>
</header>
</div>
{/* Main content */}
<div className="mt-4 w-full md:mt-0">
<div className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 flex-row xl:mr-0 xl:w-[75%]">
{/* Schedule container */}
<div className="border-subtle mb-6 rounded-md border">
<div className="flex flex-col gap-4 p-2 sm:p-4">
{/* Days of the week */}
{Array(7)
.fill(0)
.map((_, i) => (
<div
key={i}
className="flex w-full flex-col gap-4 last:mb-0 sm:flex-row sm:gap-6 sm:px-0">
{/* Day toggle and name */}
<div className="flex h-[36px] items-center justify-between sm:w-32">
<div>
<div className="text-default flex flex-row items-center space-x-2 rtl:space-x-reverse">
<div className="mr-2 h-6 w-11 rounded-full bg-gray-200" /> {/* Toggle */}
<SkeletonText className="h-5 w-16 text-sm" /> {/* Day name */}
</div>
</div>
</div>
{/* Time inputs - conditional rendering based on day being active */}
{i !== 0 &&
i !== 6 && ( // Only show time inputs for Monday-Friday
<div className="flex sm:gap-2">
<div className="flex flex-col gap-2">
<div className="flex gap-1 last:mb-0 sm:gap-2">
<div className="flex flex-row gap-2 sm:gap-3">
<SkeletonText className="h-9 w-[90px] rounded-md sm:w-[100px]" />{" "}
{/* Time input */}
<span className="text-default w-2 self-center"> - </span>
<SkeletonText className="h-9 w-[90px] rounded-md sm:w-[100px]" />{" "}
{/* Time input */}
</div>
<SkeletonText className="h-9 w-9 rounded-md" /> {/* Add button */}
</div>
</div>
<SkeletonText className="h-9 w-9 rounded-md" /> {/* Copy button */}
</div>
)}
</div>
))}
</div>
</div>
{/* Date overrides section */}
<div className="p-6">
<div className="flex items-center">
<SkeletonText className="h-5 w-32 font-medium" /> {/* Date overrides */}
<SkeletonText className="ml-2 h-4 w-4 rounded-full" /> {/* Info icon */}
</div>
<SkeletonText className="mb-4 mt-1 h-4 w-full max-w-md text-sm" /> {/* Description */}
<div className="space-y-2">
<SkeletonButton className="h-9 w-36 rounded-md" /> {/* Add an override button */}
</div>
</div>
</div>
{/* Sidebar - only visible on desktop */}
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1 xl:w-[25%]">
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
<div>
<SkeletonText className="mb-1 h-4 w-24 text-sm" /> {/* Timezone */}
<SkeletonText className="mt-1 h-9 w-72 rounded-md" /> {/* Timezone selector */}
</div>
<hr className="border-subtle my-6 mr-8" />
<div className="rounded-md">
<SkeletonText className="h-5 w-64 text-sm" /> {/* Something doesn't look right? */}
<div className="mt-3 flex">
<SkeletonButton className="h-9 w-40 rounded-md" /> {/* Launch troubleshooter */}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</SkeletonContainer>
</Shell>
);
}
@@ -1,51 +1,36 @@
"use client";
import { useRouter } from "next/navigation";
import { revalidateSchedulePage } from "app/(use-page-wrapper)/availability/[schedule]/actions";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { AvailabilitySettings } from "@calcom/atoms/availability/AvailabilitySettings";
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { ScheduleRepository } from "@calcom/lib/server/repository/schedule";
import type { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { showToast } from "@calcom/ui/components/toast";
type PageProps = {
scheduleFetched?: Awaited<ReturnType<typeof ScheduleRepository.findDetailedScheduleById>>;
travelSchedules?: Awaited<ReturnType<typeof TravelScheduleRepository.findTravelSchedulesByUserId>>;
scheduleData: RouterOutputs["viewer"]["availability"]["schedule"]["get"];
travelSchedulesData: RouterOutputs["viewer"]["travelSchedules"]["get"];
};
export const AvailabilitySettingsWebWrapper = ({
scheduleFetched: scheduleProp,
travelSchedules: travelSchedulesProp,
scheduleData: schedule,
travelSchedulesData: travelSchedules,
}: PageProps) => {
const searchParams = useCompatSearchParams();
const searchParams = useSearchParams();
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useUtils();
const me = useMeQuery();
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
const fromEventType = searchParams?.get("fromEventType");
const scheduleId = schedule.id;
const { timeFormat } = me.data || { timeFormat: null };
const { data: scheduleData, isPending: isFetchingPending } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{
enabled: !!scheduleId && !scheduleProp,
}
);
const isPending = isFetchingPending && !scheduleProp;
const schedule = scheduleProp ?? scheduleData;
const { data: travelSchedulesData } = trpc.viewer.travelSchedules.get.useQuery(undefined, {
enabled: !travelSchedulesProp,
});
const travelSchedules = travelSchedulesProp ?? travelSchedulesData;
const [isBulkUpdateModalOpen, setIsBulkUpdateModalOpen] = useState(false);
const bulkUpdateDefaultAvailabilityMutation =
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation();
@@ -57,7 +42,7 @@ export const AvailabilitySettingsWebWrapper = ({
bulkUpdateDefaultAvailabilityMutation.mutate(
{
eventTypeIds,
selectedDefaultScheduleId: scheduleData?.id,
selectedDefaultScheduleId: scheduleId,
},
{
onSuccess: () => {
@@ -86,6 +71,7 @@ export const AvailabilitySettingsWebWrapper = ({
}
}
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
revalidateSchedulePage(scheduleId);
utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
@@ -115,20 +101,12 @@ export const AvailabilitySettingsWebWrapper = ({
},
});
// TODO: reimplement Skeletons for this page in here
if (isPending) return null;
// We wait for the schedule to be loaded before rendering the form inside AvailabilitySettings
// since `defaultValues` cannot be redeclared after first render and using `values` will
// trigger a form reset when revalidating. Introducing flaky behavior.
if (!schedule) return null;
return (
<AvailabilitySettings
schedule={schedule}
travelSchedules={isDefaultSchedule ? travelSchedules || [] : []}
isDeleting={deleteMutation.isPending}
isLoading={isPending}
isLoading={false}
isSaving={updateMutation.isPending}
enableOverrides={true}
timeFormat={timeFormat}