diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a24a616af8..d9196af764 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1793,6 +1793,7 @@ "seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee", "complete_your_booking": "Complete your booking", "complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}", + "confirm_your_details": "Confirm your details", "currency_string": "{{amount, currency}}", "charge_card_dialog_body": "You are about to charge the attendee {{amount, currency}}. Are you sure you want to continue?", "charge_attendee": "Charge attendee {{amount, currency}}", diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 1815b7ed27..abde6880ab 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -14,15 +14,19 @@ import { Calendar, Columns, Grid } from "@calcom/ui/components/icon"; import { AvailableTimeSlots } from "./components/AvailableTimeSlots"; import { Away } from "./components/Away"; import { BookEventForm } from "./components/BookEventForm"; +import { BookFormAsModal } from "./components/BookEventForm/BookFormAsModal"; import { DatePicker } from "./components/DatePicker"; import { EventMeta } from "./components/EventMeta"; import { LargeCalendar } from "./components/LargeCalendar"; +import { LargeViewHeader } from "./components/LargeViewHeader"; import { BookerSection } from "./components/Section"; import { fadeInUp, fadeInLeft, resizeAnimationConfig } from "./config"; import { useBookerStore, useInitializeBookerStore } from "./store"; import type { BookerLayout, BookerProps } from "./types"; import { useEvent } from "./utils/event"; +const LargeLayouts = ["large_calendar", "large_timeslots"]; + const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => { const brandTheme = useGetBrandingColours({ lightVal: brandColor, @@ -42,11 +46,15 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book const event = useEvent(); const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); - const selectedDate = useBookerStore((state) => state.selectedDate); + const [selectedDate, setSelectedDate] = useBookerStore( + (state) => [state.selectedDate, state.setSelectedDate], + shallow + ); const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( (state) => [state.selectedTimeslot, state.setSelectedTimeslot], shallow ); + const extraDays = layout === "large_timeslots" ? (isTablet ? 2 : 4) : 0; useBrandColors({ brandColor: event.data?.profile.brandColor, @@ -63,7 +71,9 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book }); useEffect(() => { - setLayout(isMobile ? "mobile" : "small_calendar"); + if (isMobile) { + setLayout("mobile"); + } }, [isMobile, setLayout]); useEffect(() => { @@ -77,7 +87,7 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book if (layout === "mobile") { timeslotsRef.current?.scrollIntoView({ behavior: "smooth" }); } - }, [layout, selectedDate]); + }, [layout]); return ( <> @@ -89,7 +99,7 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
setLayout(layout as BookerLayout)} - defaultValue="small_calendar" + defaultValue={layout} options={[ { value: "small_calendar", @@ -143,7 +153,7 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book area="main" className="border-subtle sticky top-0 ml-[-1px] h-full p-6 md:w-[var(--booker-main-width)] md:border-l" {...fadeInUp} - visible={bookerState === "booking"}> + visible={bookerState === "booking" && layout !== "large_timeslots"}> setSelectedTimeslot(null)} /> @@ -174,17 +184,18 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book area={{ default: "main", small_calendar: "timeslots" }} visible={ (layout !== "large_calendar" && bookerState === "selecting_time") || - (layout === "large_timeslots" && bookerState !== "booking") + layout === "large_timeslots" } className={classNames( - "border-subtle flex h-full w-full flex-row p-6 pb-0 md:border-l", + "border-subtle flex h-full w-full flex-col p-6 pb-0 md:border-l", layout === "small_calendar" && "h-full overflow-auto md:w-[var(--booker-timeslots-width)]", layout !== "small_calendar" && "sticky top-0" )} ref={timeslotsRef} {...fadeInLeft}> + {layout === "large_timeslots" && } @@ -198,6 +209,11 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
+ + setSelectedTimeslot(null)} + /> ); }; diff --git a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx index 8cb8711a04..96aae6d7b9 100644 --- a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx +++ b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx @@ -55,8 +55,7 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeslot }:
{schedule.isLoading ? // Shows exact amount of days as skeleton. diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index 27f07f6d4f..507aecffa9 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -283,7 +283,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => { rescheduleUid={rescheduleUid || undefined} /> -
+
{!!onCancel && (
+
+ ); +} diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 7716e575c1..359d84d426 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -5,7 +5,7 @@ import dayjs from "@calcom/dayjs"; import type { GetBookingType } from "../lib/get-booking"; import type { BookerState, BookerLayout } from "./types"; -import { updateQueryParam, getQueryParam } from "./utils/query-param"; +import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query-param"; /** * Arguments passed into store initializer, containing @@ -51,6 +51,7 @@ type BookerStore = { */ selectedDate: string | null; setSelectedDate: (date: string | null) => void; + addToSelectedDate: (days: number) => void; /** * Selected event duration in minutes. */ @@ -80,6 +81,12 @@ type BookerStore = { initialize: (data: StoreInitializeType) => void; }; +const validLayouts: BookerLayout[] = ["large_calendar", "large_timeslots", "small_calendar"]; + +const checkLayout = (layout: BookerLayout) => { + return validLayouts.find((validLayout) => validLayout === layout); +}; + /** * The booker store contains the data of the component's * current state. This data can be reused within child components @@ -90,13 +97,33 @@ type BookerStore = { export const useBookerStore = create((set, get) => ({ state: "loading", setState: (state: BookerState) => set({ state }), - layout: "small_calendar", - setLayout: (layout: BookerLayout) => set({ layout }), + layout: checkLayout(getQueryParam("layout") as BookerLayout) || "small_calendar", + setLayout: (layout: BookerLayout) => { + // If we switch to a large layout and don't have a date selected yet, + // we selected it here, so week title is rendered properly. + if (["large_calendar", "large_timeslots"].includes(get().layout) && !get().selectedDate) { + set({ selectedDate: dayjs().format("YYYY-MM-DD") }); + } + return set({ layout }); + }, selectedDate: getQueryParam("date") || null, setSelectedDate: (selectedDate: string | null) => { set({ selectedDate }); updateQueryParam("date", selectedDate ?? ""); }, + addToSelectedDate: (days: number) => { + const currentSelection = dayjs(get().selectedDate); + const newSelection = currentSelection.add(days, "day"); + const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); + + if (newSelection.month() !== currentSelection.month()) { + set({ month: newSelection.format("YYYY-MM") }); + updateQueryParam("month", newSelection.format("YYYY-MM")); + } + + set({ selectedDate: newSelectionFormatted }); + updateQueryParam("date", newSelectionFormatted); + }, username: null, eventSlug: null, eventId: null, @@ -136,6 +163,7 @@ export const useBookerStore = create((set, get) => ({ // force clear this. if (rescheduleBooking) set({ selectedTimeslot: null }); if (month) set({ month }); + removeQueryParam("layout"); }, selectedDuration: Number(getQueryParam("duration")) || null, setSelectedDuration: (selectedDuration: number | null) => { diff --git a/packages/features/bookings/Booker/utils/query-param.ts b/packages/features/bookings/Booker/utils/query-param.ts index efa7617371..e068cc51f9 100644 --- a/packages/features/bookings/Booker/utils/query-param.ts +++ b/packages/features/bookings/Booker/utils/query-param.ts @@ -11,3 +11,11 @@ export const getQueryParam = (param: string) => { return new URLSearchParams(window.location.search).get(param); }; + +export const removeQueryParam = (param: string) => { + if (typeof window === "undefined") return; + + const url = new URL(window.location.href); + url.searchParams.delete(param); + window.history.pushState({}, "", url.href); +}; diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index bd8ce620d3..6489724a5f 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -1,3 +1,6 @@ +import { CalendarX2 } from "lucide-react"; +import { shallow } from "zustand/shallow"; + import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import type { Slots } from "@calcom/features/schedules"; @@ -6,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { nameOfDay } from "@calcom/lib/weekday"; import { Button, SkeletonText } from "@calcom/ui"; +import { useBookerStore } from "../Booker/store"; import { useTimePreferences } from "../lib"; import { TimeFormatToggle } from "./TimeFormatToggle"; @@ -29,17 +33,23 @@ export const AvailableTimes = ({ const { t, i18n } = useLocale(); const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); const hasTimeSlots = !!seatsPerTimeslot; + const [layout] = useBookerStore((state) => [state.layout], shallow); + const isLargeTimeslots = layout === "large_timeslots"; + const isToday = dayjs().isSame(date, "day"); return ( -
-
- - - {nameOfDay(i18n.language, Number(date.format("d")), "short")}, +
+
+ + + {nameOfDay(i18n.language, Number(date.format("d")), "short")} - - {" "} - {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")} + + {date.format("DD")} @@ -49,11 +59,14 @@ export const AvailableTimes = ({
)}
-
+
{!slots.length && ( -

- {t("all_booked_today")} -

+
+ +

+ {t("all_booked_today")} +

+
)} {slots.map((slot) => { @@ -65,7 +78,7 @@ export const AvailableTimes = ({ data-testid="time" data-time={slot.time} onClick={() => onTimeSelect(slot.time)} - className="mb-3 flex h-auto min-h-[44px] w-full flex-col items-center justify-center py-2" + className="mb-3 flex h-auto min-h-[44px] w-full flex-col justify-center py-2" color="secondary"> {dayjs.utc(slot.time).tz(timezone).format(timeFormat)} {bookingFull &&

{t("booking_full")}

} diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 23d8b08156..ff4bd32453 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -109,6 +109,7 @@ export const DialogContent = React.forwardRef
)} + {!type && children} );