feat: Column View Available Slots Fix (#9815)
Co-authored-by: Vinoth Kumar V <vinoth_kumar_v@Vinoths-MacBook-Pro.local> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
Vinoth Kumar V
Peer Richelsen
Udit Takkar
alannnc
Shivam Kalra
Omar López
parent
9f647ead46
commit
d6740503be
@@ -5,7 +5,9 @@ import StickyBox from "react-sticky-box";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
@@ -74,6 +76,9 @@ const BookerComponent = ({
|
||||
// In Embed we give preference to embed configuration for the layout.If that's not set, we use the App configuration for the event layout
|
||||
// But if it's mobile view, there is only one layout supported which is 'mobile'
|
||||
const layout = isEmbed ? (isMobile ? "mobile" : validateLayout(embedUiConfig.layout) || _layout) : _layout;
|
||||
const columnViewExtraDays = useRef<number>(
|
||||
isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop
|
||||
);
|
||||
|
||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
@@ -87,9 +92,36 @@ const BookerComponent = ({
|
||||
shallow
|
||||
);
|
||||
|
||||
const date = dayjs(selectedDate).format("YYYY-MM-DD");
|
||||
const schedule = useScheduleForEvent({ prefetchNextMonth: true });
|
||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||
);
|
||||
|
||||
const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
|
||||
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
|
||||
const animationScope = useBookerResizeAnimation(layout, bookerState);
|
||||
const totalWeekDays = 7;
|
||||
const addonDays =
|
||||
nonEmptyScheduleDays.length < extraDays
|
||||
? (extraDays - nonEmptyScheduleDays.length + 1) * totalWeekDays
|
||||
: nonEmptyScheduleDays.length === extraDays
|
||||
? totalWeekDays
|
||||
: 0;
|
||||
|
||||
//Taking one more avaliable slot(extraDays + 1) to claculate the no of days in between, that next and prev button need to shift.
|
||||
const availableSlots = nonEmptyScheduleDays.slice(0, extraDays + 1);
|
||||
if (nonEmptyScheduleDays.length !== 0)
|
||||
columnViewExtraDays.current =
|
||||
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 2], "day")) + addonDays;
|
||||
const prefetchNextMonth =
|
||||
dayjs(date).month() !== dayjs(date).add(columnViewExtraDays.current, "day").month();
|
||||
const monthCount =
|
||||
dayjs(date).add(1, "month").month() !== dayjs(date).add(columnViewExtraDays.current, "day").month()
|
||||
? 2
|
||||
: undefined;
|
||||
const nextSlots =
|
||||
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 1], "day")) + addonDays;
|
||||
|
||||
// I would expect isEmbed to be not needed here as it's handled in derived variable layout, but somehow removing it breaks the views.
|
||||
const defaultLayout = isEmbed
|
||||
@@ -214,8 +246,9 @@ const BookerComponent = ({
|
||||
)}>
|
||||
<Header
|
||||
enabledLayouts={bookerLayouts.enabledLayouts}
|
||||
extraDays={extraDays}
|
||||
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
|
||||
isMobile={isMobile}
|
||||
nextSlots={nextSlots}
|
||||
/>
|
||||
</BookerSection>
|
||||
<StickyOnDesktop
|
||||
@@ -281,7 +314,7 @@ const BookerComponent = ({
|
||||
layout === BookerLayouts.COLUMN_VIEW
|
||||
}
|
||||
className={classNames(
|
||||
"border-subtle rtl:border-default flex h-full w-full flex-col px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
|
||||
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
|
||||
layout === BookerLayouts.MONTH_VIEW &&
|
||||
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
|
||||
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
|
||||
@@ -291,6 +324,8 @@ const BookerComponent = ({
|
||||
<AvailableTimeSlots
|
||||
extraDays={extraDays}
|
||||
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
||||
prefetchNextMonth={prefetchNextMonth}
|
||||
monthCount={monthCount}
|
||||
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
|
||||
/>
|
||||
</BookerSection>
|
||||
|
||||
@@ -3,16 +3,21 @@ import { useMemo, useRef, useEffect } from "react";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
||||
import { useSlotsForMultipleDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import { useEvent, useScheduleForEvent } from "../utils/event";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type AvailableTimeSlotsProps = {
|
||||
extraDays?: number;
|
||||
limitHeight?: boolean;
|
||||
prefetchNextMonth: boolean;
|
||||
monthCount: number | undefined;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
};
|
||||
|
||||
@@ -23,7 +28,8 @@ type AvailableTimeSlotsProps = {
|
||||
* will also fetch the next `extraDays` days and show multiple days
|
||||
* in columns next to each other.
|
||||
*/
|
||||
export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }: AvailableTimeSlotsProps) => {
|
||||
export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot, prefetchNextMonth, monthCount}: AvailableTimeSlotsProps) => {
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
@@ -31,6 +37,8 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
|
||||
const isEmbed = useIsEmbed();
|
||||
const event = useEvent();
|
||||
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||
const [layout] = useBookerStore((state) => [state.layout]);
|
||||
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const onTimeSelect = (
|
||||
@@ -57,29 +65,19 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
|
||||
};
|
||||
|
||||
const schedule = useScheduleForEvent({
|
||||
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
});
|
||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots)
|
||||
const nonEmptyScheduleDaysFromSelectedDate = nonEmptyScheduleDays.filter((slot)=>dayjs(selectedDate).diff(slot,'day')<=0);
|
||||
|
||||
// Creates an array of dates to fetch slots for.
|
||||
// If `extraDays` is passed in, we will extend the array with the next `extraDays` days.
|
||||
const dates = useMemo(
|
||||
() =>
|
||||
!extraDays
|
||||
? [date]
|
||||
: [
|
||||
// If NO date is selected yet, we show by default the upcomming `nextDays` days.
|
||||
date,
|
||||
...Array.from({ length: extraDays }).map((_, index) =>
|
||||
dayjs(date)
|
||||
.add(index + 1, "day")
|
||||
.format("YYYY-MM-DD")
|
||||
),
|
||||
],
|
||||
[date, extraDays]
|
||||
);
|
||||
|
||||
const isMultipleDates = dates.length > 1;
|
||||
const slotsPerDay = useSlotsForMultipleDates(dates, schedule?.data?.slots);
|
||||
const dates = !extraDays
|
||||
? [date]: nonEmptyScheduleDaysFromSelectedDate.length > 0
|
||||
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays):[];
|
||||
|
||||
const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmbed) return;
|
||||
@@ -103,13 +101,15 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
|
||||
<AvailableTimes
|
||||
className="w-full"
|
||||
key={slots.date}
|
||||
showTimeFormatToggle={!isColumnView}
|
||||
onTimeSelect={onTimeSelect}
|
||||
date={dayjs(slots.date)}
|
||||
slots={slots.slots}
|
||||
onTimeSelect={onTimeSelect}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
showTimeFormatToggle={!isMultipleDates}
|
||||
availableMonth={dayjs(selectedDate).format("MM")!==dayjs(slots.date).format("MM")?dayjs(slots.date).format("MMM"):undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ export function Header({
|
||||
extraDays,
|
||||
isMobile,
|
||||
enabledLayouts,
|
||||
nextSlots,
|
||||
}: {
|
||||
extraDays: number;
|
||||
isMobile: boolean;
|
||||
enabledLayouts: BookerLayouts[];
|
||||
nextSlots: number;
|
||||
}) {
|
||||
const { t, i18n } = useLocale();
|
||||
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
||||
@@ -56,8 +58,7 @@ export function Header({
|
||||
if (isMonthView) {
|
||||
return <LayoutToggleWithData />;
|
||||
}
|
||||
|
||||
const endDate = selectedDate.add(extraDays - 1, "days");
|
||||
const endDate = selectedDate.add(layout === BookerLayouts.COLUMN_VIEW ? extraDays : extraDays - 1, "days");
|
||||
|
||||
const isSameMonth = () => {
|
||||
return selectedDate.format("MMM") === endDate.format("MMM");
|
||||
@@ -91,7 +92,7 @@ export function Header({
|
||||
color="minimal"
|
||||
StartIcon={ChevronLeft}
|
||||
aria-label="Previous Day"
|
||||
onClick={() => addToSelectedDate(-extraDays)}
|
||||
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)}
|
||||
/>
|
||||
<Button
|
||||
className="group rtl:mr-1 rtl:rotate-180"
|
||||
@@ -99,7 +100,7 @@ export function Header({
|
||||
color="minimal"
|
||||
StartIcon={ChevronRight}
|
||||
aria-label="Next Day"
|
||||
onClick={() => addToSelectedDate(extraDays)}
|
||||
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)}
|
||||
/>
|
||||
{selectedDateMin3DaysDifference && (
|
||||
<Button
|
||||
|
||||
@@ -224,7 +224,7 @@ export const extraDaysConfig = {
|
||||
tablet: 4,
|
||||
},
|
||||
[BookerLayouts.COLUMN_VIEW]: {
|
||||
desktop: 4,
|
||||
desktop: 6,
|
||||
tablet: 2,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export const useScheduleForEvent = ({
|
||||
eventId,
|
||||
month,
|
||||
duration,
|
||||
monthCount,
|
||||
}: {
|
||||
prefetchNextMonth?: boolean;
|
||||
username?: string | null;
|
||||
@@ -52,6 +53,7 @@ export const useScheduleForEvent = ({
|
||||
eventId?: number | null;
|
||||
month?: string | null;
|
||||
duration?: number | null;
|
||||
monthCount?: number;
|
||||
} = {}) => {
|
||||
const { timezone } = useTimePreferences();
|
||||
const event = useEvent();
|
||||
@@ -70,6 +72,7 @@ export const useScheduleForEvent = ({
|
||||
eventId: event.data?.id ?? eventId,
|
||||
timezone,
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
rescheduleUid,
|
||||
month: monthFromStore ?? month,
|
||||
duration: durationFromStore ?? duration,
|
||||
|
||||
@@ -26,6 +26,7 @@ type AvailableTimesProps = {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
showTimeFormatToggle?: boolean;
|
||||
className?: string;
|
||||
availableMonth?: String | undefined;
|
||||
selectedSlots?: string[];
|
||||
};
|
||||
|
||||
@@ -36,6 +37,7 @@ export const AvailableTimes = ({
|
||||
seatsPerTimeSlot,
|
||||
showTimeFormatToggle = true,
|
||||
className,
|
||||
availableMonth,
|
||||
selectedSlots,
|
||||
}: AvailableTimesProps) => {
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -64,7 +66,7 @@ export const AvailableTimes = ({
|
||||
"inline-flex items-center justify-center rounded-3xl px-1 pt-0.5 font-medium",
|
||||
isMonthView ? "text-default text-sm" : "text-xs"
|
||||
)}>
|
||||
{date.format("DD")}
|
||||
{date.format("DD")}{availableMonth && `, ${availableMonth}`}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -134,3 +136,4 @@ export const AvailableTimesSkeleton = () => (
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ type UseScheduleWithCacheArgs = {
|
||||
timezone?: string | null;
|
||||
prefetchNextMonth?: boolean;
|
||||
duration?: number | null;
|
||||
monthCount?: number | null;
|
||||
rescheduleUid?: string | null;
|
||||
isTeamEvent?: boolean;
|
||||
};
|
||||
@@ -22,11 +23,12 @@ export const useSchedule = ({
|
||||
eventId,
|
||||
prefetchNextMonth,
|
||||
duration,
|
||||
monthCount,
|
||||
rescheduleUid,
|
||||
isTeamEvent,
|
||||
}: UseScheduleWithCacheArgs) => {
|
||||
const monthDayjs = month ? dayjs(month) : dayjs();
|
||||
const nextMonthDayjs = monthDayjs.add(1, "month");
|
||||
const nextMonthDayjs = monthDayjs.add(monthCount ? monthCount : 1, "month");
|
||||
// Why the non-null assertions? All of these arguments are checked in the enabled condition,
|
||||
// and the query will not run if they are null. However, the check in `enabled` does
|
||||
// no satisfy typscript.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { Slots } from "./types";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
/**
|
||||
* Get's slots for a specific date from the schedul cache.
|
||||
* @param date Format YYYY-MM-DD
|
||||
* @param scheduleCache Instance of useScheduleWithCache
|
||||
*/
|
||||
|
||||
export const useSlotsForDate = (date: string | null, slots?: Slots) => {
|
||||
const slotsForDate = useMemo(() => {
|
||||
if (!date || typeof slots === "undefined") return [];
|
||||
@@ -16,9 +18,10 @@ export const useSlotsForDate = (date: string | null, slots?: Slots) => {
|
||||
return slotsForDate;
|
||||
};
|
||||
|
||||
export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots) => {
|
||||
export const useSlotsForAvailableDates = ( dates: (string | null)[], slots?: Slots) => {
|
||||
|
||||
const slotsForDates = useMemo(() => {
|
||||
if (typeof slots === "undefined") return [];
|
||||
if (slots === undefined) return [];
|
||||
return dates
|
||||
.filter((date) => date !== null)
|
||||
.map((date) => ({
|
||||
@@ -26,6 +29,5 @@ export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots
|
||||
date,
|
||||
}));
|
||||
}, [dates, slots]);
|
||||
|
||||
return slotsForDates;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user