8c39210a12
* chore: add new view for two step slot selection for embed * fix: make sure two step slot selection is false by default * chore: update embed playground to showcase two step slot selection * chore: add tests * chore: update embed snippet generator code to include two step slot selection * chore: update docs * fix: back button styling * chore: implement feedback from cubic * fix: add missing isEnableTwoStepSlotSelectionVisible properties to test mock store Co-Authored-By: unknown <> * chore: implement PR feedback * chore: update slot selection modal * chore: add prop to hide available times header * fix: scope two-step slot selection visibility to mobile only Restores the isMobile check in the timeslot visibility guard to ensure desktop embeds continue to show the booking UI when two-step slot selection is enabled. The feature should only hide the timeslot list on mobile devices. Addresses Cubic AI review feedback (confidence 9/10). Co-Authored-By: unknown <> * fixup: use translations for loading instead of actual word * fix: type check * fix: add window.matchMedia mock for jsdom test environment Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: add window.matchMedia mock to packages/testing/src/setupVitest.ts Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: unit tests failing * add a width style to the two-step slot selection embed container. * refactor: rename enableTwoStepSlotSelection to useSlotsViewOnSmallScreen - Rename external-facing embed config option from enableTwoStepSlotSelection to useSlotsViewOnSmallScreen - Update URL parameter parsing in embed-iframe.ts - Update type definitions in types.ts and index.d.ts - Update internal variable names to slotsViewOnSmallScreen for consistency - Update documentation, playground examples, and tests Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Add config to prefill all booking fields so that slot has confirm button along with it * chore: implement PR feedback * fix: move twoStepSlotSelection embed outside misc-embeds div to fix e2e test The e2e test was failing because the misc-embeds div content was intercepting pointer events on mobile viewport. This follows the same pattern used for skeletonDemo - the embed is now outside misc-embeds and the div is hidden when testing this namespace. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: prevent pointer event interception in twoStepSlotSelection embed Hide heading and note elements and set pointer-events: none on container elements while keeping pointer-events: auto on the iframe container. This prevents the container from intercepting clicks on the iframe during mobile viewport e2e tests. Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: hide all non-essential content for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: disable pointer-events on body for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: remove pointer-events: auto on container to let clicks pass through to iframe Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: explicitly set pointer-events: none on container and inline-embed-container Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: also set pointer-events: none on html element to prevent interception Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: make .place div fill viewport for twoStepSlotSelection e2e test Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: failing embed tests * fixup * fixup fixup * fix: failing tests * fix: update checks for verifying prefilled date --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
843 lines
20 KiB
TypeScript
843 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { createWithEqualityFn } from "zustand/traditional";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { BOOKER_NUMBER_OF_DAYS_TO_LOAD } from "@calcom/lib/constants";
|
|
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
|
|
|
import type { GetBookingType } from "../lib/get-booking";
|
|
import type { BookerState, BookerLayout } from "./types";
|
|
import {
|
|
updateQueryParam,
|
|
getQueryParam,
|
|
removeQueryParam,
|
|
} from "./utils/query-param";
|
|
|
|
const _iso_3166_1_alpha_2_codes = [
|
|
"ad",
|
|
"ae",
|
|
"af",
|
|
"ag",
|
|
"ai",
|
|
"al",
|
|
"am",
|
|
"ao",
|
|
"aq",
|
|
"ar",
|
|
"as",
|
|
"at",
|
|
"au",
|
|
"aw",
|
|
"ax",
|
|
"az",
|
|
"ba",
|
|
"bb",
|
|
"bd",
|
|
"be",
|
|
"bf",
|
|
"bg",
|
|
"bh",
|
|
"bi",
|
|
"bj",
|
|
"bl",
|
|
"bm",
|
|
"bn",
|
|
"bo",
|
|
"bq",
|
|
"br",
|
|
"bs",
|
|
"bt",
|
|
"bv",
|
|
"bw",
|
|
"by",
|
|
"bz",
|
|
"ca",
|
|
"cc",
|
|
"cd",
|
|
"cf",
|
|
"cg",
|
|
"ch",
|
|
"ci",
|
|
"ck",
|
|
"cl",
|
|
"cm",
|
|
"cn",
|
|
"co",
|
|
"cr",
|
|
"cu",
|
|
"cv",
|
|
"cw",
|
|
"cx",
|
|
"cy",
|
|
"cz",
|
|
"de",
|
|
"dj",
|
|
"dk",
|
|
"dm",
|
|
"do",
|
|
"dz",
|
|
"ec",
|
|
"ee",
|
|
"eg",
|
|
"eh",
|
|
"er",
|
|
"es",
|
|
"et",
|
|
"fi",
|
|
"fj",
|
|
"fk",
|
|
"fm",
|
|
"fo",
|
|
"fr",
|
|
"ga",
|
|
"gb",
|
|
"gd",
|
|
"ge",
|
|
"gf",
|
|
"gg",
|
|
"gh",
|
|
"gi",
|
|
"gl",
|
|
"gm",
|
|
"gn",
|
|
"gp",
|
|
"gq",
|
|
"gr",
|
|
"gs",
|
|
"gt",
|
|
"gu",
|
|
"gw",
|
|
"gy",
|
|
"hk",
|
|
"hm",
|
|
"hn",
|
|
"hr",
|
|
"ht",
|
|
"hu",
|
|
"id",
|
|
"ie",
|
|
"il",
|
|
"im",
|
|
"in",
|
|
"io",
|
|
"iq",
|
|
"ir",
|
|
"is",
|
|
"it",
|
|
"je",
|
|
"jm",
|
|
"jo",
|
|
"jp",
|
|
"ke",
|
|
"kg",
|
|
"kh",
|
|
"ki",
|
|
"km",
|
|
"kn",
|
|
"kp",
|
|
"kr",
|
|
"kw",
|
|
"ky",
|
|
"kz",
|
|
"la",
|
|
"lb",
|
|
"lc",
|
|
"li",
|
|
"lk",
|
|
"lr",
|
|
"ls",
|
|
"lt",
|
|
"lu",
|
|
"lv",
|
|
"ly",
|
|
"ma",
|
|
"mc",
|
|
"md",
|
|
"me",
|
|
"mf",
|
|
"mg",
|
|
"mh",
|
|
"mk",
|
|
"ml",
|
|
"mm",
|
|
"mn",
|
|
"mo",
|
|
"mp",
|
|
"mq",
|
|
"mr",
|
|
"ms",
|
|
"mt",
|
|
"mu",
|
|
"mv",
|
|
"mw",
|
|
"mx",
|
|
"my",
|
|
"mz",
|
|
"na",
|
|
"nc",
|
|
"ne",
|
|
"nf",
|
|
"ng",
|
|
"ni",
|
|
"nl",
|
|
"no",
|
|
"np",
|
|
"nr",
|
|
"nu",
|
|
"nz",
|
|
"om",
|
|
"pa",
|
|
"pe",
|
|
"pf",
|
|
"pg",
|
|
"ph",
|
|
"pk",
|
|
"pl",
|
|
"pm",
|
|
"pn",
|
|
"pr",
|
|
"ps",
|
|
"pt",
|
|
"pw",
|
|
"py",
|
|
"qa",
|
|
"re",
|
|
"ro",
|
|
"rs",
|
|
"ru",
|
|
"rw",
|
|
"sa",
|
|
"sb",
|
|
"sc",
|
|
"sd",
|
|
"se",
|
|
"sg",
|
|
"sh",
|
|
"si",
|
|
"sj",
|
|
"sk",
|
|
"sl",
|
|
"sm",
|
|
"sn",
|
|
"so",
|
|
"sr",
|
|
"ss",
|
|
"st",
|
|
"sv",
|
|
"sx",
|
|
"sy",
|
|
"tc",
|
|
"td",
|
|
"tf",
|
|
"tg",
|
|
"th",
|
|
"tj",
|
|
"tk",
|
|
"tl",
|
|
"tm",
|
|
"tn",
|
|
"to",
|
|
"tr",
|
|
"tt",
|
|
"tv",
|
|
"tw",
|
|
"tz",
|
|
"ua",
|
|
"ug",
|
|
"um",
|
|
"us",
|
|
"uy",
|
|
"uz",
|
|
"va",
|
|
"vc",
|
|
"ve",
|
|
"vg",
|
|
"vi",
|
|
"vn",
|
|
"vu",
|
|
"wf",
|
|
"ws",
|
|
"ye",
|
|
"yt",
|
|
"za",
|
|
"zm",
|
|
"zw",
|
|
] as const;
|
|
|
|
export type CountryCode = (typeof _iso_3166_1_alpha_2_codes)[number];
|
|
|
|
/**
|
|
* Arguments passed into store initializer, containing
|
|
* the event data.
|
|
*/
|
|
export type StoreInitializeType = {
|
|
username: string;
|
|
eventSlug: string;
|
|
// Month can be undefined if it's not passed in as a prop.
|
|
eventId: number | undefined;
|
|
layout: BookerLayout;
|
|
month?: string;
|
|
bookingUid?: string | null;
|
|
isTeamEvent?: boolean;
|
|
bookingData?: GetBookingType | null | undefined;
|
|
verifiedEmail?: string | null;
|
|
rescheduleUid?: string | null;
|
|
rescheduledBy?: string | null;
|
|
seatReferenceUid?: string;
|
|
durationConfig?: number[] | null;
|
|
org?: string | null;
|
|
isInstantMeeting?: boolean;
|
|
timezone?: string | null;
|
|
teamMemberEmail?: string | null;
|
|
crmOwnerRecordType?: string | null;
|
|
crmAppSlug?: string | null;
|
|
crmRecordId?: string | null;
|
|
isPlatform?: boolean;
|
|
allowUpdatingUrlParams?: boolean;
|
|
defaultPhoneCountry?: CountryCode;
|
|
};
|
|
|
|
type SeatedEventData = {
|
|
seatsPerTimeSlot?: number | null;
|
|
attendees?: number;
|
|
bookingUid?: string;
|
|
showAvailableSeatsCount?: boolean | null;
|
|
};
|
|
|
|
export type BookerStore = {
|
|
/**
|
|
* Event details. These are stored in store for easier
|
|
* access in child components.
|
|
*/
|
|
username: string | null;
|
|
eventSlug: string | null;
|
|
eventId: number | null;
|
|
/**
|
|
* Verified booker email.
|
|
* Needed in case user turns on Requires Booker Email Verification for an event
|
|
*/
|
|
verifiedEmail: string | null;
|
|
setVerifiedEmail: (email: string | null) => void;
|
|
/**
|
|
* Verification code for email verification.
|
|
* Stored after successful verification to be included in booking request
|
|
*/
|
|
verificationCode: string | null;
|
|
setVerificationCode: (code: string | null) => void;
|
|
/**
|
|
* Current month being viewed. Format is YYYY-MM.
|
|
*/
|
|
month: string | null;
|
|
setMonth: (month: string | null) => void;
|
|
/**
|
|
* Current state of the booking process
|
|
* the user is currently in. See enum for possible values.
|
|
*/
|
|
state: BookerState;
|
|
setState: (state: BookerState) => void;
|
|
/**
|
|
* The booker component supports different layouts,
|
|
* this value tracks the current layout.
|
|
*/
|
|
layout: BookerLayout;
|
|
setLayout: (layout: BookerLayout) => void;
|
|
/**
|
|
* Date selected by user (exact day). Format is YYYY-MM-DD.
|
|
*/
|
|
selectedDate: string | null;
|
|
setSelectedDate: (params: {
|
|
date: string | null;
|
|
omitUpdatingParams?: boolean;
|
|
preventMonthSwitching?: boolean;
|
|
}) => void;
|
|
addToSelectedDate: (days: number) => void;
|
|
/**
|
|
* Multiple Selected Dates and Times
|
|
*/
|
|
selectedDatesAndTimes: { [key: string]: { [key: string]: string[] } } | null;
|
|
setSelectedDatesAndTimes: (selectedDatesAndTimes: {
|
|
[key: string]: { [key: string]: string[] };
|
|
}) => void;
|
|
/**
|
|
* Multiple duration configuration
|
|
*/
|
|
durationConfig: number[] | null;
|
|
/**
|
|
* Selected event duration in minutes.
|
|
*/
|
|
selectedDuration: number | null;
|
|
setSelectedDuration: (duration: number | null) => void;
|
|
/**
|
|
* Selected timeslot user has chosen. This is a date string
|
|
* containing both the date + time.
|
|
*/
|
|
selectedTimeslot: string | null;
|
|
setSelectedTimeslot: (timeslot: string | null) => void;
|
|
tentativeSelectedTimeslots: string[];
|
|
setTentativeSelectedTimeslots: (slots: string[]) => void;
|
|
/**
|
|
* Number of recurring events to create.
|
|
*/
|
|
recurringEventCount: number | null;
|
|
setRecurringEventCount(count: number | null): void;
|
|
/**
|
|
* Input occurrence count.
|
|
*/
|
|
recurringEventCountQueryParam: number | null;
|
|
setRecurringEventCountQueryParam(count: number | null): void;
|
|
/**
|
|
* The number of days worth of schedules to load.
|
|
*/
|
|
dayCount: number | null;
|
|
setDayCount: (dayCount: number | null) => void;
|
|
/**
|
|
* If booking is being rescheduled or it has seats, it receives a rescheduleUid with rescheduledBy or bookingUid
|
|
* the current booking details are passed in. The `bookingData`
|
|
* object is something that's fetched server side.
|
|
*/
|
|
rescheduleUid: string | null;
|
|
setRescheduleUid: (rescheduleUid: string | null) => void;
|
|
rescheduledBy: string | null;
|
|
bookingUid: string | null;
|
|
bookingData: GetBookingType | null;
|
|
setBookingData: (bookingData: GetBookingType | null | undefined) => void;
|
|
|
|
/**
|
|
* Method called by booker component to set initial data.
|
|
*/
|
|
initialize: (data: StoreInitializeType) => void;
|
|
/**
|
|
* Stored form state, used when user navigates back and
|
|
* forth between timeslots and form. Gets cleared on submit
|
|
* to prevent sticky data.
|
|
*/
|
|
formValues: Record<string, any>;
|
|
setFormValues: (values: Record<string, any>) => void;
|
|
/**
|
|
* Force event being a team event, so we only query for team events instead
|
|
* of also include 'user' events and return the first event that matches with
|
|
* both the slug and the event slug.
|
|
*/
|
|
isTeamEvent: boolean;
|
|
seatedEventData: SeatedEventData;
|
|
setSeatedEventData: (seatedEventData: SeatedEventData) => void;
|
|
|
|
isInstantMeeting?: boolean;
|
|
|
|
org?: string | null;
|
|
setOrg: (org: string | null | undefined) => void;
|
|
|
|
timezone: string | null;
|
|
setTimezone: (timezone: string | null) => void;
|
|
|
|
teamMemberEmail?: string | null;
|
|
crmOwnerRecordType?: string | null;
|
|
crmAppSlug?: string | null;
|
|
crmRecordId?: string | null;
|
|
isPlatform?: boolean;
|
|
allowUpdatingUrlParams?: boolean;
|
|
defaultPhoneCountry?: CountryCode | null;
|
|
/**
|
|
* Whether the two-step slot selection modal/dialog is visible
|
|
*/
|
|
isSlotSelectionModalVisible: boolean;
|
|
setIsSlotSelectionModalVisible: (visible: boolean) => void;
|
|
};
|
|
|
|
/**
|
|
* Creates a new booker store instance
|
|
*/
|
|
export const createBookerStore = () =>
|
|
createWithEqualityFn<BookerStore>((set, get) => ({
|
|
state: "loading",
|
|
setState: (state: BookerState) => set({ state }),
|
|
layout: BookerLayouts.MONTH_VIEW,
|
|
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 (
|
|
["week_view", "column_view"].includes(layout) &&
|
|
!get().selectedDate
|
|
) {
|
|
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
|
|
}
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("layout", layout);
|
|
}
|
|
return set({ layout });
|
|
},
|
|
selectedDate: getQueryParam("date") || null,
|
|
setSelectedDate: ({
|
|
date: selectedDate,
|
|
omitUpdatingParams = false,
|
|
preventMonthSwitching = false,
|
|
}) => {
|
|
// unset selected date
|
|
if (!selectedDate) {
|
|
removeQueryParam("date");
|
|
return;
|
|
}
|
|
|
|
const currentSelection = dayjs(get().selectedDate);
|
|
const newSelection = dayjs(selectedDate);
|
|
set({ selectedDate });
|
|
if (
|
|
!omitUpdatingParams &&
|
|
(!get().isPlatform || get().allowUpdatingUrlParams)
|
|
) {
|
|
updateQueryParam("date", selectedDate ?? "");
|
|
}
|
|
|
|
// Setting month make sure small calendar in fullscreen layouts also updates.
|
|
// preventMonthSwitching is true in monthly view
|
|
if (
|
|
!preventMonthSwitching &&
|
|
newSelection.month() !== currentSelection.month()
|
|
) {
|
|
set({ month: newSelection.format("YYYY-MM") });
|
|
if (
|
|
!omitUpdatingParams &&
|
|
(!get().isPlatform || get().allowUpdatingUrlParams)
|
|
) {
|
|
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
|
}
|
|
}
|
|
},
|
|
selectedDatesAndTimes: null,
|
|
setSelectedDatesAndTimes: (selectedDatesAndTimes) => {
|
|
set({ selectedDatesAndTimes });
|
|
},
|
|
addToSelectedDate: (days: number) => {
|
|
const currentSelection = dayjs(get().selectedDate);
|
|
let newSelection = currentSelection.add(days, "day");
|
|
|
|
// If newSelection is before the current date, set it to today
|
|
if (newSelection.isBefore(dayjs(), "day")) {
|
|
newSelection = dayjs();
|
|
}
|
|
|
|
const newSelectionFormatted = newSelection.format("YYYY-MM-DD");
|
|
|
|
if (newSelection.month() !== currentSelection.month()) {
|
|
set({ month: newSelection.format("YYYY-MM") });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
|
}
|
|
}
|
|
|
|
set({ selectedDate: newSelectionFormatted });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("date", newSelectionFormatted);
|
|
}
|
|
},
|
|
username: null,
|
|
eventSlug: null,
|
|
eventId: null,
|
|
rescheduledBy: null,
|
|
verifiedEmail: null,
|
|
setVerifiedEmail: (email: string | null) => {
|
|
set({ verifiedEmail: email });
|
|
},
|
|
verificationCode: null,
|
|
setVerificationCode: (code: string | null) => {
|
|
set({ verificationCode: code });
|
|
},
|
|
month:
|
|
getQueryParam("month") ||
|
|
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid()
|
|
? dayjs(getQueryParam("date")).format("YYYY-MM")
|
|
: null) ||
|
|
dayjs().format("YYYY-MM"),
|
|
setMonth: (month: string | null) => {
|
|
if (!month) {
|
|
removeQueryParam("month");
|
|
return;
|
|
}
|
|
set({ month, selectedTimeslot: null });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("month", month ?? "");
|
|
}
|
|
get().setSelectedDate({ date: null });
|
|
},
|
|
dayCount:
|
|
BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
|
|
setDayCount: (dayCount: number | null) => {
|
|
set({ dayCount });
|
|
},
|
|
isTeamEvent: false,
|
|
seatedEventData: {
|
|
seatsPerTimeSlot: undefined,
|
|
attendees: undefined,
|
|
bookingUid: undefined,
|
|
showAvailableSeatsCount: true,
|
|
},
|
|
setSeatedEventData: (seatedEventData: SeatedEventData) => {
|
|
set({ seatedEventData });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
|
|
}
|
|
},
|
|
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
|
|
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
|
|
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
|
|
timezone: getQueryParam("cal.tz") ?? null,
|
|
setTimezone: (timezone: string | null) => {
|
|
set({ timezone });
|
|
},
|
|
initialize: ({
|
|
username,
|
|
eventSlug,
|
|
month,
|
|
eventId,
|
|
rescheduleUid = null,
|
|
rescheduledBy = null,
|
|
bookingUid = null,
|
|
bookingData = null,
|
|
layout,
|
|
isTeamEvent,
|
|
durationConfig,
|
|
org,
|
|
isInstantMeeting,
|
|
timezone = null,
|
|
teamMemberEmail,
|
|
crmOwnerRecordType,
|
|
crmAppSlug,
|
|
crmRecordId,
|
|
isPlatform = false,
|
|
allowUpdatingUrlParams = true,
|
|
defaultPhoneCountry,
|
|
}: StoreInitializeType) => {
|
|
const selectedDateInStore = get().selectedDate;
|
|
|
|
if (
|
|
get().username === username &&
|
|
get().eventSlug === eventSlug &&
|
|
get().month === month &&
|
|
get().eventId === eventId &&
|
|
get().rescheduleUid === rescheduleUid &&
|
|
get().bookingUid === bookingUid &&
|
|
get().bookingData?.responses.email === bookingData?.responses.email &&
|
|
get().layout === layout &&
|
|
get().timezone === timezone &&
|
|
get().rescheduledBy === rescheduledBy &&
|
|
get().teamMemberEmail === teamMemberEmail &&
|
|
get().crmOwnerRecordType === crmOwnerRecordType &&
|
|
get().crmAppSlug === crmAppSlug &&
|
|
get().crmRecordId === crmRecordId
|
|
)
|
|
return;
|
|
set({
|
|
username,
|
|
eventSlug,
|
|
eventId,
|
|
org,
|
|
rescheduleUid,
|
|
rescheduledBy,
|
|
bookingUid,
|
|
bookingData,
|
|
layout: layout || BookerLayouts.MONTH_VIEW,
|
|
isTeamEvent: isTeamEvent || false,
|
|
durationConfig,
|
|
timezone,
|
|
// Preselect today's date in week / column view, since they use this to show the week title.
|
|
selectedDate:
|
|
selectedDateInStore ||
|
|
(["week_view", "column_view"].includes(layout)
|
|
? dayjs().format("YYYY-MM-DD")
|
|
: null),
|
|
teamMemberEmail,
|
|
crmOwnerRecordType,
|
|
crmAppSlug,
|
|
crmRecordId,
|
|
isPlatform,
|
|
allowUpdatingUrlParams,
|
|
defaultPhoneCountry,
|
|
});
|
|
|
|
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
|
|
set({
|
|
selectedDuration: Number(getQueryParam("duration")),
|
|
});
|
|
} else {
|
|
removeQueryParam("duration");
|
|
}
|
|
|
|
// Unset selected timeslot if user is rescheduling. This could happen
|
|
// if the user reschedules a booking right after the confirmation page.
|
|
// In that case the time would still be store in the store, this way we
|
|
// force clear this.
|
|
if (rescheduleUid && bookingData) {
|
|
set({ selectedTimeslot: null });
|
|
}
|
|
if (month) set({ month });
|
|
|
|
if (isInstantMeeting) {
|
|
const month = dayjs().format("YYYY-MM");
|
|
const selectedDate = dayjs().format("YYYY-MM-DD");
|
|
const selectedTimeslot = new Date().toISOString();
|
|
set({
|
|
month,
|
|
selectedDate,
|
|
selectedTimeslot,
|
|
isInstantMeeting,
|
|
});
|
|
|
|
if (!isPlatform || allowUpdatingUrlParams) {
|
|
updateQueryParam("month", month);
|
|
updateQueryParam("date", selectedDate ?? "");
|
|
updateQueryParam("slot", selectedTimeslot ?? "", false);
|
|
}
|
|
}
|
|
//removeQueryParam("layout");
|
|
},
|
|
durationConfig: null,
|
|
selectedDuration: null,
|
|
setSelectedDuration: (selectedDuration: number | null) => {
|
|
set({ selectedDuration });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("duration", selectedDuration ?? "");
|
|
}
|
|
},
|
|
setBookingData: (bookingData: GetBookingType | null | undefined) => {
|
|
set({ bookingData: bookingData ?? null });
|
|
},
|
|
setRescheduleUid: (rescheduleUid: string | null) => {
|
|
set({ rescheduleUid });
|
|
},
|
|
recurringEventCount: null,
|
|
setRecurringEventCount: (recurringEventCount: number | null) =>
|
|
set({ recurringEventCount }),
|
|
recurringEventCountQueryParam:
|
|
Number(getQueryParam("recurringEventCount")) || null,
|
|
setRecurringEventCountQueryParam: (
|
|
recurringEventCountQueryParam: number | null
|
|
) => {
|
|
// Guard: only update state if value is valid (not NaN or null)
|
|
if (
|
|
recurringEventCountQueryParam !== null &&
|
|
!isNaN(recurringEventCountQueryParam)
|
|
) {
|
|
set({ recurringEventCountQueryParam });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam(
|
|
"recurringEventCount",
|
|
recurringEventCountQueryParam
|
|
);
|
|
}
|
|
}
|
|
// If invalid, don't update state or URL - just ignore the call
|
|
},
|
|
rescheduleUid: null,
|
|
bookingData: null,
|
|
bookingUid: null,
|
|
selectedTimeslot: getQueryParam("slot") || null,
|
|
tentativeSelectedTimeslots: [],
|
|
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => {
|
|
set({ tentativeSelectedTimeslots });
|
|
},
|
|
setSelectedTimeslot: (selectedTimeslot: string | null) => {
|
|
set({ selectedTimeslot });
|
|
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
|
updateQueryParam("slot", selectedTimeslot ?? "", false);
|
|
}
|
|
},
|
|
formValues: {},
|
|
setFormValues: (formValues: Record<string, any>) => {
|
|
set({ formValues });
|
|
},
|
|
org: null,
|
|
setOrg: (org: string | null | undefined) => {
|
|
set({ org });
|
|
},
|
|
isPlatform: false,
|
|
allowUpdatingUrlParams: true,
|
|
defaultPhoneCountry: null,
|
|
isSlotSelectionModalVisible: false,
|
|
setIsSlotSelectionModalVisible: (
|
|
isSlotSelectionModalVisible: boolean
|
|
) => {
|
|
set({ isSlotSelectionModalVisible });
|
|
},
|
|
}));
|
|
|
|
/**
|
|
* Default global store instance for backward compatibility
|
|
*/
|
|
export const useBookerStore = createBookerStore();
|
|
|
|
export const useInitializeBookerStore = ({
|
|
username,
|
|
eventSlug,
|
|
month,
|
|
eventId,
|
|
rescheduleUid = null,
|
|
rescheduledBy = null,
|
|
bookingData = null,
|
|
verifiedEmail = null,
|
|
layout,
|
|
isTeamEvent,
|
|
durationConfig,
|
|
org,
|
|
isInstantMeeting,
|
|
timezone = null,
|
|
teamMemberEmail,
|
|
crmOwnerRecordType,
|
|
crmAppSlug,
|
|
crmRecordId,
|
|
isPlatform = false,
|
|
allowUpdatingUrlParams = true,
|
|
defaultPhoneCountry,
|
|
}: StoreInitializeType) => {
|
|
const initializeStore = useBookerStore((state) => state.initialize);
|
|
useEffect(() => {
|
|
initializeStore({
|
|
username,
|
|
eventSlug,
|
|
month,
|
|
eventId,
|
|
rescheduleUid,
|
|
rescheduledBy,
|
|
bookingData,
|
|
layout,
|
|
isTeamEvent,
|
|
org,
|
|
verifiedEmail,
|
|
durationConfig,
|
|
isInstantMeeting,
|
|
timezone,
|
|
teamMemberEmail,
|
|
crmOwnerRecordType,
|
|
crmAppSlug,
|
|
crmRecordId,
|
|
isPlatform,
|
|
allowUpdatingUrlParams,
|
|
defaultPhoneCountry,
|
|
});
|
|
}, [
|
|
initializeStore,
|
|
org,
|
|
username,
|
|
eventSlug,
|
|
month,
|
|
eventId,
|
|
rescheduleUid,
|
|
rescheduledBy,
|
|
bookingData,
|
|
layout,
|
|
isTeamEvent,
|
|
verifiedEmail,
|
|
durationConfig,
|
|
isInstantMeeting,
|
|
timezone,
|
|
teamMemberEmail,
|
|
crmOwnerRecordType,
|
|
crmAppSlug,
|
|
crmRecordId,
|
|
isPlatform,
|
|
allowUpdatingUrlParams,
|
|
defaultPhoneCountry,
|
|
]);
|
|
};
|