Files
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
73f51920c5 refactor: move Booker hooks from packages/features to apps/web/modules (#27343)
* refactor: move Booker hooks from packages/features to apps/web/modules

Migrate the following Booker hooks:
- useOverlayCalendar
- useSkipConfirmStep
- useInitializeWeekStart
- useIsQuickAvailabilityCheckFeatureEnabled
- useDecoyBooking

These hooks are only imported by apps/web files, making them safe to move
without creating circular dependencies.

Part of the tRPC-driven UI migration from packages/features to apps/web/modules.

Co-Authored-By: [email protected] <[email protected]>

* fix: revert useIsQuickAvailabilityCheckFeatureEnabled move and fix import paths

- Reverted useIsQuickAvailabilityCheckFeatureEnabled.ts back to packages/features since it's imported by useSlots.ts in packages/features (would create circular dependency)
- Fixed import paths in useOverlayCalendar.ts to use @calcom/features paths
- Fixed import paths in useSkipConfirmStep.ts to use @calcom/features paths
- Updated Booker.tsx to import useIsQuickAvailabilityCheckFeatureEnabled from original location

Co-Authored-By: [email protected] <[email protected]>

* fix: use @calcom/web alias for hook imports to fix vitest resolution

Co-Authored-By: [email protected] <[email protected]>

* refactor

* migrate more hooks

* fix

* mv types

* fix

* fix

* fix

* fix

* revert: remove formatting-only changes to make PR easier to review

Co-Authored-By: [email protected] <[email protected]>

* fix

* fix

* fix

* revert: restore original import ordering in moved hooks

Co-Authored-By: [email protected] <[email protected]>

* fix imports

* fix

* fix

* wip

* wip

* wip

* wip

* wip

* wip

* fix: add isBrowser guard to useSlotsViewOnSmallScreen hook

Address Cubic AI review feedback (confidence 9/10) by adding the isBrowser
guard to the useSlotsViewOnSmallScreen hook to follow the file's client-only
contract and avoid executing during SSR/prerendering.

Co-Authored-By: unknown <>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-31 07:33:33 -03:00

140 lines
4.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringDates } from "@calcom/lib/parse-dates";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { getTimeShiftFlags, getFirstShiftFlags } from "@calcom/lib/timeShift";
import { Alert } from "@calcom/ui/components/alert";
import { Badge } from "@calcom/ui/components/badge";
import { Input } from "@calcom/ui/components/form";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime";
export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurringEvent"> }) => {
const maxOccurences = event.recurringEvent?.count || null;
const { t, i18n } = useLocale();
const [
setRecurringEventCount,
recurringEventCount,
setRecurringEventCountQueryParam,
recurringEventCountQueryParam,
] = useBookerStoreContext((state) => [
state.setRecurringEventCount,
state.recurringEventCount,
state.setRecurringEventCountQueryParam,
state.recurringEventCountQueryParam,
]);
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const bookerState = useBookerStoreContext((state) => state.state);
const { timezone, timeFormat } = useBookerTime();
const [warning, setWarning] = useState(false);
const validateAndSetRecurringEventCount = (value: number | string) => {
const inputValue = parseInt(value as string);
const isValid =
!isNaN(inputValue) && inputValue >= 1 && maxOccurences !== null && inputValue <= maxOccurences;
if (isValid) {
setRecurringEventCount(inputValue);
setWarning(false);
} else {
setRecurringEventCount(maxOccurences);
setWarning(true);
}
};
useEffect(() => {
if (!event.recurringEvent?.count) return;
if (recurringEventCountQueryParam) {
validateAndSetRecurringEventCount(recurringEventCountQueryParam);
} else {
setRecurringEventCount(maxOccurences);
setRecurringEventCountQueryParam(maxOccurences);
}
}, [setRecurringEventCount, event.recurringEvent, recurringEventCount, recurringEventCountQueryParam]);
if (!event.recurringEvent) return null;
if (bookerState === "booking" && recurringEventCount && selectedTimeslot) {
const [recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: selectedTimeslot,
timeZone: timezone,
recurringEvent: event.recurringEvent,
recurringCount: recurringEventCount,
selectedTimeFormat: timeFormat,
},
i18n.language
);
const shiftFlags = getTimeShiftFlags({ dates: recurringDates, timezone });
const displayFlags = getFirstShiftFlags(shiftFlags);
return (
<div data-testid="recurring-dates">
{recurringStrings.slice(0, 5).map((timeFormatted, key) => (
<p key={key}>
{timeFormatted}
{displayFlags[key] && (
<>
{" "}
<Badge variant="orange" size="sm">
{t("time_shift")}
</Badge>
</>
)}
</p>
))}
{recurringStrings.length > 5 && (
<Tooltip
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>
{timeFormatted}
{displayFlags[key + 5] && (
<>
{" "}
<Badge variant="orange" size="sm">
{t("time_shift")}
</Badge>
</>
)}
</p>
))}>
<p className=" text-sm">+ {t("plus_more", { count: recurringStrings.length - 5 })}</p>
</Tooltip>
)}
</div>
);
}
return (
<>
{getRecurringFreq({ t, recurringEvent: event.recurringEvent })}
<br />
<Input
className="my-1 mr-3 inline-flex h-[26px] w-[46px] px-1 py-0"
type="number"
min="1"
max={event.recurringEvent.count}
defaultValue={recurringEventCountQueryParam || event.recurringEvent.count}
data-testid="occurrence-input"
onChange={(event) => {
const inputValue = parseInt(event.target.value);
setRecurringEventCountQueryParam(inputValue);
validateAndSetRecurringEventCount(event.target.value);
}}
/>
{t("occurrence", {
count: recurringEventCount || event.recurringEvent.count,
})}
{warning && (
<div className="-ml-4 mr-4 mt-2 flex">
<Alert severity="warning" title={t("enter_number_between_range", { maxOccurences })} />
</div>
)}
</>
);
};