diff --git a/packages/config/tailwind-preset.js b/packages/config/tailwind-preset.js index 2c05681336..87388f0352 100644 --- a/packages/config/tailwind-preset.js +++ b/packages/config/tailwind-preset.js @@ -96,9 +96,13 @@ module.exports = { from: { opacity: 0, transform: "translateY(10px)" }, to: { opacity: 1, transform: "none" }, }, + spinning: { + "100%": { transform: "rotate(360deg)" }, + }, }, animation: { "fade-in-up": "fade-in-up 600ms var(--animation-delay, 0ms) cubic-bezier(.21,1.02,.73,1) forwards", + spinning: "spinning 0.75s linear infinite", }, boxShadow: { dropdown: "0px 2px 6px -1px rgba(0, 0, 0, 0.08)", diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 558f889196..02e62f08ab 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -58,7 +58,7 @@ const BookerComponent = ({ const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings; const animationScope = useBookerResizeAnimation(layout, bookerState); const isEmbed = typeof window !== "undefined" && window?.isEmbed?.(); - const isMonthView = layout === BookerLayouts.MONTH_VIEW; + // We only want the initial url value, that's why we memo it. The embed seems to change the url, which sometimes drops // the layout query param. const layoutFromQueryParam = useMemo(() => validateLayout(getQueryParam("layout") as BookerLayouts), []); @@ -109,21 +109,27 @@ const BookerComponent = ({ return ( <> -
+
- +
state.selectedDuration); const { data } = useEvent(); const parsedSelectedTimeslot = dayjs(selectedTimeslot); + const { timeFormat } = useTimePreferences(); return ( @@ -24,7 +26,9 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan

{t("confirm_your_details")}

- {parsedSelectedTimeslot.format("LLL")} + + {parsedSelectedTimeslot.format("LL")} {parsedSelectedTimeslot.format(timeFormat)} + {(selectedDuration || data?.length) && ( diff --git a/packages/features/bookings/Booker/components/LargeCalendar.tsx b/packages/features/bookings/Booker/components/LargeCalendar.tsx index d935923151..b312f22821 100644 --- a/packages/features/bookings/Booker/components/LargeCalendar.tsx +++ b/packages/features/bookings/Booker/components/LargeCalendar.tsx @@ -4,43 +4,53 @@ import dayjs from "@calcom/dayjs"; import { Calendar } from "@calcom/features/calendars/weeklyview"; import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state"; +import { useTimePreferences } from "../../lib/timePreferences"; import { useBookerStore } from "../store"; -import { useScheduleForEvent } from "../utils/event"; +import { useEvent, useScheduleForEvent } from "../utils/event"; export const LargeCalendar = ({ extraDays }: { extraDays: number }) => { const selectedDate = useBookerStore((state) => state.selectedDate); const date = selectedDate || dayjs().format("YYYY-MM-DD"); const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); + const selectedEventDuration = useBookerStore((state) => state.selectedDuration); const schedule = useScheduleForEvent({ prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(), }); + const { timezone } = useTimePreferences(); + + const event = useEvent(); + const eventDuration = selectedEventDuration || event?.data?.length || 30; const availableSlots = useMemo(() => { const availableTimeslots: CalendarAvailableTimeslots = {}; if (!schedule.data) return availableTimeslots; if (!schedule.data.slots) return availableTimeslots; + for (const day in schedule.data.slots) { availableTimeslots[day] = schedule.data.slots[day].map((slot) => ({ - start: dayjs(slot.time).toDate(), - end: dayjs(slot.time).add(30, "minutes").toDate(), + // First formatting to LLL and then passing it to date prevents toDate() + // from changing the timezone to users local machine (instead of itmezone selected in UI dropdown) + start: new Date(dayjs(slot.time).utc().tz(timezone).format("LLL")), + end: new Date(dayjs(slot.time).utc().tz(timezone).add(eventDuration, "minutes").format("LLL")), })); } return availableTimeslots; - }, [schedule]); + }, [schedule, timezone, eventDuration]); return ( -
+
setSelectedTimeslot(date.toString())} - gridCellsPerHour={2} - hoverEventDuration={30} + gridCellsPerHour={60 / eventDuration} + hoverEventDuration={eventDuration} hideHeader />
diff --git a/packages/features/bookings/Booker/config.ts b/packages/features/bookings/Booker/config.ts index 80eb11c6f4..9c19bdd313 100644 --- a/packages/features/bookings/Booker/config.ts +++ b/packages/features/bookings/Booker/config.ts @@ -87,8 +87,8 @@ export const resizeAnimationConfig: ResizeAnimationConfig = { week_view: { default: { width: "100vw", - minHeight: "450px", - height: "100vh", + minHeight: "100vh", + height: "auto", gridTemplateAreas: ` "meta header header" "meta main main" @@ -100,8 +100,8 @@ export const resizeAnimationConfig: ResizeAnimationConfig = { column_view: { default: { width: "100vw", - minHeight: "450px", - height: "100vh", + minHeight: "100vh", + height: "auto", gridTemplateAreas: ` "meta header header" "meta main main" diff --git a/packages/features/calendars/weeklyview/components/Calendar.tsx b/packages/features/calendars/weeklyview/components/Calendar.tsx index 5b808d2606..ebf007f568 100644 --- a/packages/features/calendars/weeklyview/components/Calendar.tsx +++ b/packages/features/calendars/weeklyview/components/Calendar.tsx @@ -1,15 +1,19 @@ -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef } from "react"; + +import { classNames } from "@calcom/lib"; import { useCalendarStore } from "../state/store"; import "../styles/styles.css"; import type { CalendarComponentProps } from "../types/state"; -import { calculateHourSizeInPx, getDaysBetweenDates, getHoursToDisplay } from "../utils"; +import { getDaysBetweenDates, getHoursToDisplay } from "../utils"; import { DateValues } from "./DateValues"; -import { EmptyCell } from "./event/Empty"; +import { CurrentTime } from "./currentTime"; +import { AvailableCellsForDay, EmptyCell } from "./event/Empty"; import { EventList } from "./event/EventList"; import { SchedulerColumns } from "./grid"; import { SchedulerHeading } from "./heading/SchedulerHeading"; import { HorizontalLines } from "./horizontalLines"; +import { Spinner } from "./spinner/Spinner"; import { VeritcalLines } from "./verticalLines"; export function Calendar(props: CalendarComponentProps) { @@ -28,26 +32,9 @@ export function Calendar(props: CalendarComponentProps) { const hideHeader = useCalendarStore((state) => state.hideHeader); const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]); - const hours = useMemo(() => getHoursToDisplay(startHour || 0, endHour || 23), [startHour, endHour]); - const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour; - const [hourSize, setHourSize] = useState(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour)); - - // Reset one hour size on window resize. - useLayoutEffect(() => { - const onResize = () => { - setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour)); - }; - window.addEventListener("resize", onResize); - - // By running this set function also one time in the uselayouteffect, instead of - // only in the default value of useState, we make sure that the ref is rendered - // to the screen and we read the correct value. - setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour)); - - return () => window.removeEventListener("resize", onResize); - }, [startHour, endHour]); + const hourSize = 58; // Initalise State on inital mount useEffect(() => { @@ -57,7 +44,7 @@ export function Calendar(props: CalendarComponentProps) { return (
{hideHeader !== true && } + {props.isLoading && }
@@ -72,13 +60,8 @@ export function Calendar(props: CalendarComponentProps) { style={{ width: "165%" }} className="flex h-full max-w-full flex-none flex-col sm:max-w-none md:max-w-full"> - {/* TODO: Implement this at a later date. */} - {/* */} -
+
+
- {/* Empty Cells */} - - <> - {[...Array(days.length)].map((_, i) => ( -
  • - {/* While startDate < endDate: */} - {[...Array(numberOfGridStopsPerDay)].map((_, j) => { - const key = `${i}-${j}`; - return ( - - ); - })} -
  • - ))} - -
    - @@ -141,6 +90,49 @@ export function Calendar(props: CalendarComponentProps) { ); })} + + {/* Empty Cells */} + + <> + {[...Array(days.length)].map((_, i) => ( +
  • + {/* While startDate < endDate: */} + {availableTimeslots ? ( + + ) : ( + <> + {[...Array(numberOfGridStopsPerDay)].map((_, j) => { + const key = `${i}-${j}`; + return ( + + ); + })} + + )} +
  • + ))} + +
    diff --git a/packages/features/calendars/weeklyview/components/DateValues/index.tsx b/packages/features/calendars/weeklyview/components/DateValues/index.tsx index c104b8abb0..5a38b4568f 100644 --- a/packages/features/calendars/weeklyview/components/DateValues/index.tsx +++ b/packages/features/calendars/weeklyview/components/DateValues/index.tsx @@ -12,7 +12,7 @@ export function DateValues({ days, containerNavRef }: Props) { return (
    + className="bg-default dark:bg-muted border-b-subtle sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] flex-none border-b sm:pr-8">
    {days.map((day) => { const isToday = dayjs().isSame(day, "day"); diff --git a/packages/features/calendars/weeklyview/components/currentTime/index.tsx b/packages/features/calendars/weeklyview/components/currentTime/index.tsx index 962efe299c..e47a531be0 100644 --- a/packages/features/calendars/weeklyview/components/currentTime/index.tsx +++ b/packages/features/calendars/weeklyview/components/currentTime/index.tsx @@ -1,43 +1,54 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import dayjs from "@calcom/dayjs"; +import { useTimePreferences } from "@calcom/features/bookings/lib"; import { useCalendarStore } from "../../state/store"; -type Props = { - containerNavRef: React.RefObject; - containerRef: React.RefObject; - containerOffsetRef: React.RefObject; -}; - -export function CurrentTime({ containerOffsetRef }: Props) { - const [currentTimePos, setCurrentTimePos] = useState(0); +export function CurrentTime() { + const currentTimeRef = useRef(null); + const [scrolledIntoView, setScrolledIntoView] = useState(false); + const [currentTimePos, setCurrentTimePos] = useState(null); const { startHour, endHour } = useCalendarStore((state) => ({ startHour: state.startHour || 0, endHour: state.endHour || 23, })); + const { timeFormat } = useTimePreferences(); useEffect(() => { // Set the container scroll position based on the current time. + const currentHour = new Date().getHours(); let currentMinute = new Date().getHours() * 60; currentMinute = currentMinute + new Date().getMinutes(); - if (containerOffsetRef.current) { - const totalHours = endHour - startHour; - const currentTimePos = currentMinute; - setCurrentTimePos(currentTimePos); + if (currentHour > endHour || currentHour < startHour) { + setCurrentTimePos(null); } + + const minutesFromStart = currentMinute - startHour * 60; + setCurrentTimePos(minutesFromStart); + + if (!currentTimeRef.current || scrolledIntoView) return; + // Within a small timeout so element has time to render. + setTimeout(() => { + currentTimeRef?.current?.scrollIntoView({ block: "center" }); + setScrolledIntoView(true); + }, 100); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [startHour, endHour]); + }, [startHour, endHour, scrolledIntoView]); return (