diff --git a/apps/storybook/styles/globals.css b/apps/storybook/styles/globals.css index e375a7ba752e6a..115058d60a7108 100644 --- a/apps/storybook/styles/globals.css +++ b/apps/storybook/styles/globals.css @@ -2,4 +2,9 @@ @tailwind components; @tailwind utilities; -@import "../../../packages/ui/styles/shared-globals.css" +@import "../../../packages/ui/styles/shared-globals.css"; + +:root { + --font-inter: "Inter var"; + --font-cal: "Cal Sans"; +} diff --git a/apps/web/components/Embed.tsx b/apps/web/components/Embed.tsx index 503e50b8dafeff..462a8cb7daecfd 100644 --- a/apps/web/components/Embed.tsx +++ b/apps/web/components/Embed.tsx @@ -20,10 +20,10 @@ import { Switch, TextArea, TextField, + ColorPicker, } from "@calcom/ui"; import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon"; -import ColorPicker from "@components/ui/colorpicker"; import Select from "@components/ui/form/Select"; type EmbedType = "inline" | "floating-popup" | "element-click"; diff --git a/apps/web/components/booking/AvailableTimes.tsx b/apps/web/components/booking/AvailableTimes.tsx index 2a05ba94e3e29a..cbd14dd0928cd0 100644 --- a/apps/web/components/booking/AvailableTimes.tsx +++ b/apps/web/components/booking/AvailableTimes.tsx @@ -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 = ({ seatsPerTimeSlot, ethSignature, }) => { + const [slotPickerRef] = useAutoAnimate(); const { t, i18n } = useLocale(); const router = useRouter(); const { rescheduleUid } = router.query; @@ -49,122 +51,124 @@ const AvailableTimes: FC = ({ setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim()); }, []); - if (!date) return null; - return ( -
-
-
- - {nameOfDay(i18n.language, Number(date.format("d")), "short")} - - - , {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")} - -
-
- 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") }, - ]} - /> -
-
-
- {slots.length > 0 && - slots.map((slot) => { - type BookingURL = { - pathname: string; - query: Record; - }; - 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 } : {}), - }, - }; +
+ {!!date ? ( +
+
+
+ + {nameOfDay(i18n.language, Number(date.format("d")), "short")} + + + , {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")} + +
+
+ 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") }, + ]} + /> +
+
+
+ {slots.length > 0 && + slots.map((slot) => { + type BookingURL = { + pathname: string; + query: Record; + }; + 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 (rescheduleUid) { + bookingUrl.query.rescheduleUid = rescheduleUid as string; + } - // If event already has an attendee add booking id - if (slot.bookingUid) { - bookingUrl.query.bookingUid = slot.bookingUid; - } + // If event already has an attendee add booking id + if (slot.bookingUid) { + bookingUrl.query.bookingUid = slot.bookingUid; + } - return ( -
- {/* ^ 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 ? ( -
- {dayjs(slot.time).tz(timeZone()).format(timeFormat)} - {!!seatsPerTimeSlot &&

{t("booking_full")}

} -
- ) : ( - + {/* ^ 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 ? ( +
+ {dayjs(slot.time).tz(timeZone()).format(timeFormat)} + {!!seatsPerTimeSlot &&

{t("booking_full")}

} +
+ ) : ( + + {dayjs(slot.time).tz(timeZone()).format(timeFormat)} + {!!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")} +

+ )} + )} - data-testid="time"> - {dayjs(slot.time).tz(timeZone()).format(timeFormat)} - {!!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")} -

- )} - - )} +
+ ); + })} + + {!isLoading && !slots.length && ( +
+

{t("all_booked_today")}

- ); - })} + )} - {!isLoading && !slots.length && ( -
-

{t("all_booked_today")}

+ {isLoading && !slots.length && ( + <> + + + + + + + + + + + )}
- )} - - {isLoading && !slots.length && ( - <> - - - - - - - - - - - )} -
+
+ ) : null}
); }; diff --git a/apps/web/components/booking/BookingDescription.tsx b/apps/web/components/booking/BookingDescription.tsx index 2a639616b28a6d..5c9f067a4c9f88 100644 --- a/apps/web/components/booking/BookingDescription.tsx +++ b/apps/web/components/booking/BookingDescription.tsx @@ -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"; diff --git a/apps/web/components/booking/SlotPicker.tsx b/apps/web/components/booking/SlotPicker.tsx new file mode 100644 index 00000000000000..3fb71b6ce0d15d --- /dev/null +++ b/apps/web/components/booking/SlotPicker.tsx @@ -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 }, + "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(); + const [browsingDate, setBrowsingDate] = useState(); + 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 ( + <> + 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} + /> + + + ); +}; diff --git a/apps/web/components/booking/TimezoneDropdown.tsx b/apps/web/components/booking/TimezoneDropdown.tsx new file mode 100644 index 00000000000000..195a9acec7f835 --- /dev/null +++ b/apps/web/components/booking/TimezoneDropdown.tsx @@ -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 ( + <> +
+ + +
+ + ); +} diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index 5a76522b8e71e3..7ba03a4e717461 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -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,222 +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 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 { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery( - { - eventTypeId, - eventTypeSlug, - usernameList, - startTime: startTime?.toISOString() || "", - endTime: endTime?.toISOString() || "", - timeZone, - duration, - }, - { - enabled: !!startTime && !!endTime && enabled, - refetchInterval: 3000, - trpc: { context: { skipBatch: true } }, - } - ); - - // 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 }, - "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(); - const [browsingDate, setBrowsingDate] = useState(); - 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(); - - 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 ( - <> - 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} - /> - -
- {selectedDate ? ( - - ) : null} -
- - ); -}; - -function TimezoneDropdown({ - onChangeTimeZone, -}: { - onChangeTimeZone: (newTimeZone: string) => void; - timeZone?: string; -}) { - const handleSelectTimeZone = (newTimeZone: string) => { - onChangeTimeZone(newTimeZone); - localStorageTimeZone(newTimeZone); - }; - - return ( - <> -
- - -
- - ); -} +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: () =>
, +});*/ +const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), { + ssr: false, +}); const dateQuerySchema = z.object({ rescheduleUid: z.string().optional().default(""), diff --git a/apps/web/components/ui/colorpicker.tsx b/apps/web/components/ui/colorpicker.tsx deleted file mode 100644 index 214ed6a6f88ca1..00000000000000 --- a/apps/web/components/ui/colorpicker.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useCallback, useRef, useState } from "react"; -import { useEffect } from "react"; -import { HexColorInput, HexColorPicker } from "react-colorful"; - -import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding"; -import { Swatch } from "@calcom/ui"; - -type Handler = (event: MouseEvent | Event) => void; -function useEventListener< - KW extends keyof WindowEventMap, - KH extends keyof HTMLElementEventMap, - T extends HTMLElement | void = void ->( - eventName: KW | KH, - handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void, - element?: React.RefObject -) { - // Create a ref that stores handler - const savedHandler = useRef(); - useEffect(() => { - // Define the listening target - const targetElement: T | Window = element?.current || window; - if (!(targetElement && targetElement.addEventListener)) { - return; - } - // Update saved handler if necessary - if (savedHandler.current !== handler) { - savedHandler.current = handler; - } - // Create event listener that calls handler function stored in ref - const eventListener: typeof handler = (event) => { - // eslint-disable-next-line no-extra-boolean-cast - if (!!savedHandler?.current) { - savedHandler.current(event); - } - }; - targetElement.addEventListener(eventName, eventListener); - // Remove event listener on cleanup - return () => { - targetElement.removeEventListener(eventName, eventListener); - }; - }, [eventName, element, handler]); -} - -function useOnClickOutside( - ref: React.RefObject, - handler: Handler, - mouseEvent: "mousedown" | "mouseup" = "mousedown" -): void { - useEventListener(mouseEvent, (event) => { - const el = ref?.current; - // Do nothing if clicking ref's element or descendent elements - if (!el || el.contains(event.target as Node)) { - return; - } - handler(event); - }); -} -export type ColorPickerProps = { - defaultValue: string; - onChange: (text: string) => void; -}; - -const ColorPicker = (props: ColorPickerProps) => { - const init = !isValidHexCode(props.defaultValue) - ? fallBackHex(props.defaultValue, false) - : props.defaultValue; - const [color, setColor] = useState(init); - const [isOpen, toggle] = useState(false); - const popover = useRef() as React.MutableRefObject; - const close = useCallback(() => toggle(false), []); - useOnClickOutside(popover, close); - return ( -
- toggle(!isOpen)} /> - - {isOpen && ( -
- { - setColor(val); - props.onChange(val); - }} - /> -
- )} - { - setColor(val); - props.onChange(val); - }} - type="text" - /> -
- ); -}; - -export default ColorPicker; diff --git a/apps/web/fonts/CalSans-SemiBold.woff2 b/apps/web/fonts/CalSans-SemiBold.woff2 new file mode 100644 index 00000000000000..36d71b70d0c15e Binary files /dev/null and b/apps/web/fonts/CalSans-SemiBold.woff2 differ diff --git a/apps/web/package.json b/apps/web/package.json index a85770f5089d46..ee9298b83204ad 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,7 +44,8 @@ "@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", "@radix-ui/react-dialog": "^1.0.0", @@ -80,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", @@ -139,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", diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 11c4da78a447f8..42bd29f017d50b 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -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. diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index a086947b011339..5528fdd1452e49 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,3 +1,5 @@ +import { Inter } from "@next/font/google"; +import localFont from "@next/font/local"; import { DefaultSeo } from "next-seo"; import Head from "next/head"; import Script from "next/script"; @@ -14,6 +16,14 @@ import I18nLanguageHandler from "@components/I18nLanguageHandler"; import "../styles/globals.css"; +const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" }); +const calFont = localFont({ + src: "../fonts/CalSans-SemiBold.woff2", + variable: "--font-cal", + preload: true, + display: "swap", +}); + function MyApp(props: AppProps) { const { Component, pageProps, err, router } = props; let pageStatus = "200"; @@ -46,6 +56,12 @@ function MyApp(props: AppProps) { id="page-status" dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }} /> + diff --git a/apps/web/pages/_document.tsx b/apps/web/pages/_document.tsx index eddbff9943a3bd..cd0201dc549f1a 100644 --- a/apps/web/pages/_document.tsx +++ b/apps/web/pages/_document.tsx @@ -39,14 +39,6 @@ class MyDocument extends Document { - -