Reduce booking page bundle size (#7131)

* AvailabilityPage Component loaded dynamically

* update next/bundle-analyzer dependency

* Load dynamically SlotPicker and TimezoneDropDown and make some imports refactoring

* Gates functionality Returned

* Replace crypto with md5 dependency to generate gravatar url

* Refactor: AvailableTimes and SlotPicker

* Update next.config.js

* Update SlotPicker.tsx

* Update getEventTypeAppData.ts

---------

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Efraín Rochínandzomars authored and GitHub committed 2023-02-17 12:53:31 -07:00
1 parent 602c813928
commit fd76bc9cab
21 files changed
+533 -464

No files matched your search

+117 -113
View File
@@ -1,3 +1,4 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link";
import { useRouter } from "next/router";
import type { FC } from "react";
@@ -39,6 +40,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
seatsPerTimeSlot,
ethSignature,
}) => {
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
const { rescheduleUid } = router.query;
@@ -49,122 +51,124 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
}, []);
if (!date) return null;
return (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
<div className="mb-6 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
<div className="ml-auto">
<ToggleGroup
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
options={[
{ value: "12", label: t("12_hour_short") },
{ value: "24", label: t("24_hour_short") },
]}
/>
</div>
</div>
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined>;
};
const bookingUrl: BookingURL = {
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
query: {
...router.query,
date: dayjs.utc(slot.time).tz(timeZone()).format(),
type: eventTypeId,
slug: eventTypeSlug,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
...(ethSignature ? { ethSignature } : {}),
},
};
if (rescheduleUid) {
bookingUrl.query.rescheduleUid = rescheduleUid as string;
}
// If event already has an attendee add booking id
if (slot.bookingUid) {
bookingUrl.query.bookingUid = slot.bookingUid;
}
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
<div
className={classNames(
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
</div>
) : (
<Link
href={bookingUrl}
prefetch={false}
className={classNames(
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
<p
className={`${
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
? "text-rose-600"
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-emerald-400"
} text-sm`}>
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
{seatsPerTimeSlot} {t("seats_available")}
</p>
)}
</Link>
)}
</div>
);
})}
{!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
<div ref={slotPickerRef}>
{!!date ? (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
<div className="mb-6 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
<div className="ml-auto">
<ToggleGroup
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
options={[
{ value: "12", label: t("12_hour_short") },
{ value: "24", label: t("24_hour_short") },
]}
/>
</div>
</div>
)}
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined>;
};
const bookingUrl: BookingURL = {
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
query: {
...router.query,
date: dayjs.utc(slot.time).tz(timeZone()).format(),
type: eventTypeId,
slug: eventTypeSlug,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
...(ethSignature ? { ethSignature } : {}),
},
};
{isLoading && !slots.length && (
<>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
</>
)}
</div>
if (rescheduleUid) {
bookingUrl.query.rescheduleUid = rescheduleUid as string;
}
// If event already has an attendee add booking id
if (slot.bookingUid) {
bookingUrl.query.bookingUid = slot.bookingUid;
}
return (
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
<div
className={classNames(
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
</div>
) : (
<Link
href={bookingUrl}
prefetch={false}
className={classNames(
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
<p
className={`${
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
? "text-rose-600"
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-emerald-400"
} text-sm`}>
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
{seatsPerTimeSlot} {t("seats_available")}
</p>
)}
</Link>
)}
</div>
);
})}
{!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
</div>
)}
{isLoading && !slots.length && (
<>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
<SkeletonContainer className="mb-2">
<SkeletonText className="h-5 w-full" />
</SkeletonContainer>
</>
)}
</div>
</div>
) : null}
</div>
);
};
@@ -3,7 +3,7 @@ import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui";
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
+200
View File
@@ -0,0 +1,200 @@
import type { EventType } from "@prisma/client";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import type { z } from "zod";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import DatePicker from "@calcom/features/calendars/DatePicker";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useRouterQuery from "@lib/hooks/useRouterQuery";
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
const getRefetchInterval = (refetchCount: number): number => {
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
return intervals[refetchCount] || intervals[intervals.length - 1];
};
const useSlots = ({
eventTypeId,
eventTypeSlug,
startTime,
endTime,
usernameList,
timeZone,
duration,
enabled = true,
}: {
eventTypeId: number;
eventTypeSlug: string;
startTime?: Dayjs;
endTime?: Dayjs;
usernameList: string[];
timeZone?: string;
duration?: string;
enabled?: boolean;
}) => {
const [refetchCount, setRefetchCount] = useState(0);
const refetchInterval = getRefetchInterval(refetchCount);
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
{
eventTypeId,
eventTypeSlug,
usernameList,
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
duration,
},
{
enabled: !!startTime && !!endTime && enabled,
refetchInterval,
trpc: { context: { skipBatch: true } },
}
);
useEffect(() => {
if (!!data && fetchStatus === "idle") {
setRefetchCount(refetchCount + 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchStatus, data]);
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
};
export const SlotPicker = ({
eventType,
timeFormat,
onTimeFormatChange,
timeZone,
recurringEventCount,
users,
seatsPerTimeSlot,
weekStart = 0,
ethSignature,
}: {
eventType: Pick<
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
"id" | "schedulingType" | "slug" | "length" | "metadata"
>;
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
recurringEventCount?: number;
users: string[];
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
ethSignature?: string;
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
let { duration = eventType.length.toString() } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
if (!eventType.metadata?.multipleDuration) {
duration = eventType.length.toString();
}
useEffect(() => {
if (!router.isReady) return;
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
if (timeZone === "Etc/GMT") {
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
if (date) {
setSelectedDate(dayjs.utc(date));
}
} else {
// Set the start of the month without shifting time like startOf() may do.
setBrowsingDate(
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
);
if (date) {
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
setSelectedDate(dayjs.tz(date, timeZone));
}
}
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: monthSlots, isLoading } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime:
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
});
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
/** Prevent refetching is we already have this data from month slots */
enabled: !!selectedDate,
});
/** Hide skeleton if we have the slot loaded in the month query */
const isLoadingSelectedDateSlots = (() => {
if (!selectedDate) return _isLoadingSelectedDateSlots;
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
return false;
})();
return (
<>
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={(newDate) => {
setDate(newDate.format("YYYY-MM-DD"));
}}
onMonthChange={(newMonth) => {
setMonth(newMonth.format("YYYY-MM"));
}}
browsingDate={browsingDate}
weekStart={weekStart}
/>
<AvailableTimes
isLoading={isLoadingSelectedDateSlots}
slots={
selectedDate &&
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
monthSlots[selectedDate.format("YYYY-MM-DD")])
}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
/>
</>
);
};
@@ -0,0 +1,26 @@
import { FiGlobe } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import TimeOptions from "@components/booking/TimeOptions";
export function TimezoneDropdown({
onChangeTimeZone,
}: {
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
}) {
const handleSelectTimeZone = (newTimeZone: string) => {
onChangeTimeZone(newTimeZone);
localStorageTimeZone(newTimeZone);
};
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
);
}
@@ -1,15 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { EventType } from "@prisma/client";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useMemo, useReducer, useState } from "react";
import { Toaster } from "react-hot-toast";
import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -18,7 +14,6 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import DatePicker from "@calcom/features/calendars/DatePicker";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
@@ -28,235 +23,30 @@ import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
import { FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import type { Gate, GateState } from "@components/Gates";
import Gates from "@components/Gates";
import BookingDescription from "@components/booking/BookingDescription";
import TimeOptions from "@components/booking/TimeOptions";
import { SlotPicker } from "@components/booking/SlotPicker";
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
const getRefetchInterval = (refetchCount: number): number => {
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
return intervals[refetchCount] || intervals[intervals.length - 1];
};
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
const useSlots = ({
eventTypeId,
eventTypeSlug,
startTime,
endTime,
usernameList,
timeZone,
duration,
enabled = true,
}: {
eventTypeId: number;
eventTypeSlug: string;
startTime?: Dayjs;
endTime?: Dayjs;
usernameList: string[];
timeZone?: string;
duration?: string;
enabled?: boolean;
}) => {
const [refetchCount, setRefetchCount] = useState(0);
const refetchInterval = getRefetchInterval(refetchCount);
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
{
eventTypeId,
eventTypeSlug,
usernameList,
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
duration,
},
{
enabled: !!startTime && !!endTime && enabled,
refetchInterval,
trpc: { context: { skipBatch: true } },
}
);
useEffect(() => {
if (!!data && fetchStatus === "idle") {
setRefetchCount(refetchCount + 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchStatus, data]);
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
};
const SlotPicker = ({
eventType,
timeFormat,
onTimeFormatChange,
timeZone,
recurringEventCount,
users,
seatsPerTimeSlot,
weekStart = 0,
ethSignature,
}: {
eventType: Pick<
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
"id" | "schedulingType" | "slug" | "length" | "metadata"
>;
timeFormat: TimeFormat;
onTimeFormatChange: (is24Hour: boolean) => void;
timeZone?: string;
seatsPerTimeSlot?: number;
recurringEventCount?: number;
users: string[];
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
ethSignature?: string;
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
let { duration = eventType.length.toString() } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
if (!eventType.metadata?.multipleDuration) {
duration = eventType.length.toString();
}
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
useEffect(() => {
if (!router.isReady) return;
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
if (timeZone === "Etc/GMT") {
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
if (date) {
setSelectedDate(dayjs.utc(date));
}
} else {
// Set the start of the month without shifting time like startOf() may do.
setBrowsingDate(
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
);
if (date) {
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
setSelectedDate(dayjs.tz(date, timeZone));
}
}
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: monthSlots, isLoading } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime:
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
});
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
/** Prevent refetching is we already have this data from month slots */
enabled: !!selectedDate,
});
/** Hide skeleton if we have the slot loaded in the month query */
const isLoadingSelectedDateSlots = (() => {
if (!selectedDate) return _isLoadingSelectedDateSlots;
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
return false;
})();
return (
<>
<DatePicker
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={(newDate) => {
setDate(newDate.format("YYYY-MM-DD"));
}}
onMonthChange={(newMonth) => {
setMonth(newMonth.format("YYYY-MM"));
}}
browsingDate={browsingDate}
weekStart={weekStart}
/>
<div ref={slotPickerRef}>
{selectedDate ? (
<AvailableTimes
isLoading={isLoadingSelectedDateSlots}
slots={
selectedDate &&
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
monthSlots[selectedDate.format("YYYY-MM-DD")])
}
date={selectedDate}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
seatsPerTimeSlot={seatsPerTimeSlot}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
/>
) : null}
</div>
</>
);
};
function TimezoneDropdown({
onChangeTimeZone,
}: {
onChangeTimeZone: (newTimeZone: string) => void;
timeZone?: string;
}) {
const handleSelectTimeZone = (newTimeZone: string) => {
onChangeTimeZone(newTimeZone);
localStorageTimeZone(newTimeZone);
};
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
);
}
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
ssr: false,
loading: () => <div className="mt-8 px-4 pb-4 sm:mt-0 sm:p-4 md:min-w-[300px] md:px-5 lg:min-w-[455px]" />,
});*/
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
ssr: false,
});
const dateQuerySchema = z.object({
rescheduleUid: z.string().optional().default(""),
+3 -1
View File
@@ -44,7 +44,7 @@
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",
"@next-auth/prisma-adapter": "^1.0.4",
"@next/bundle-analyzer": "^12.2.5",
"@next/bundle-analyzer": "^13.1.6",
"@next/font": "^13.1.1",
"@radix-ui/react-avatar": "^1.0.0",
"@radix-ui/react-collapsible": "^1.0.0",
@@ -81,6 +81,7 @@
"lodash": "^4.17.21",
"lottie-react": "^2.3.1",
"markdown-it": "^13.0.1",
"md5": "^2.3.0",
"memory-cache": "^0.2.0",
"micro": "^10.0.1",
"mime-types": "^2.1.35",
@@ -140,6 +141,7 @@
"@types/glidejs__glide": "^3.4.2",
"@types/lodash": "^4.14.182",
"@types/markdown-it": "^12.2.3",
"@types/md5": "^2.3.2",
"@types/memory-cache": "^0.2.2",
"@types/micro": "7.3.7",
"@types/mime-types": "^2.1.1",
+20 -14
View File
@@ -1,21 +1,10 @@
import MarkdownIt from "markdown-it";
import type { GetStaticPaths, GetStaticPropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import prisma from "@calcom/prisma";
import type { User } from "@calcom/prisma/client";
import {
EventTypeMetaDataSchema,
teamMetadataSchema,
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -65,10 +54,19 @@ export default function Type(props: AvailabilityPageProps) {
Type.isThemeSupported = true;
const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetStaticPropsContext) {
const { type: slug, user: username } = paramsSchema.parse(context.params);
// load server side dependencies
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
const { ssgInit } = await import("@server/lib/ssg");
const { type: slug, user: username } = paramsSchema.parse(context.params);
const ssg = await ssgInit(context);
const user = await prisma.user.findUnique({
where: {
username,
@@ -191,7 +189,17 @@ async function getUserPageProps(context: GetStaticPropsContext) {
}
async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
// load server side dependencies
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
"@calcom/prisma/zod-utils"
);
const { ssgInit } = await import("@server/lib/ssg");
const { getAppFromSlug } = await import("@calcom/app-store/utils");
const ssg = await ssgInit(context);
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
const usernameList = getUsernameList(userParam);
@@ -310,8 +318,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
};
}
const paramsSchema = z.object({ type: z.string(), user: z.string() });
export const getStaticProps = async (context: GetStaticPropsContext) => {
const { user: userParam } = paramsSchema.parse(context.params);
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.