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:
Vinoth Kumar V
2023-08-24 16:39:45 -07:00
committed by GitHub
co-authored by Vinoth Kumar V Peer Richelsen Udit Takkar alannnc Shivam Kalra Omar López
parent 9f647ead46
commit d6740503be
8 changed files with 81 additions and 35 deletions
+37 -2
View File
@@ -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
+1 -1
View File
@@ -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;
};