Skip to content

Commit 7fef683

Browse files
CarinaWolliCarinaWollisean-brydoneunjae-lee
authored
feat: Improving Booking Visibility at Month-End (calcom#22770)
* remove first weeks and add last * fix added last week * prefetch availability of next month * don't switch month * On hover show month * only show new UI in monthly view * show month tooldtip only when needed * show month on first day of month * remove isFirstDayOfNextMonth * fix prefetching next month * fix datePicker tests * preventMonthSwitching in monthly view * add tests * code clean up * code clean up * code clena up for ooo days * push first day of month * remove bg color for the month badge * fix text colour * remove not needed * use object param * revert: use object param * use object param * fix DatePicker tests --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Sean Brydon <sean@cal.com> Co-authored-by: Eunjae Lee <hey@eunjae.dev>
1 parent 63df3d9 commit 7fef683

8 files changed

Lines changed: 278 additions & 41 deletions

File tree

packages/features/bookings/Booker/components/DatePicker.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export const DatePicker = ({
7474
scrollToTimeSlots?: () => void;
7575
}) => {
7676
const { i18n } = useLocale();
77-
const [month, selectedDate] = useBookerStore((state) => [state.month, state.selectedDate], shallow);
77+
const [month, selectedDate, layout] = useBookerStore(
78+
(state) => [state.month, state.selectedDate, state.layout],
79+
shallow
80+
);
7881

7982
const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
8083
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
@@ -83,7 +86,7 @@ export const DatePicker = ({
8386

8487
const onMonthChange = (date: Dayjs) => {
8588
setMonth(date.format("YYYY-MM"));
86-
setSelectedDate(date.format("YYYY-MM-DD"));
89+
setSelectedDate({ date: date.format("YYYY-MM-DD") });
8790
setDayCount(null); // Whenever the month is changed, we nullify getting X days
8891
};
8992

@@ -98,6 +101,9 @@ export const DatePicker = ({
98101
});
99102
moveToNextMonthOnNoAvailability();
100103

104+
// Determine if this is a compact sidebar view based on layout
105+
const isCompact = layout !== "month_view";
106+
101107
const periodData: PeriodData = {
102108
...{
103109
periodType: "UNLIMITED",
@@ -126,7 +132,11 @@ export const DatePicker = ({
126132
className={classNames?.datePickerContainer}
127133
isLoading={isLoading}
128134
onChange={(date: Dayjs | null, omitUpdatingParams?: boolean) => {
129-
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"), omitUpdatingParams);
135+
setSelectedDate({
136+
date: date === null ? date : date.format("YYYY-MM-DD"),
137+
omitUpdatingParams,
138+
preventMonthSwitching: !isCompact, // Prevent month switching when in monthly view
139+
});
130140
}}
131141
onMonthChange={onMonthChange}
132142
includedDates={nonEmptyScheduleDays}
@@ -137,6 +147,7 @@ export const DatePicker = ({
137147
slots={slots}
138148
scrollToTimeSlots={scrollToTimeSlots}
139149
periodData={periodData}
150+
isCompact={isCompact}
140151
/>
141152
);
142153
};

packages/features/bookings/Booker/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function Header({
130130
<Button
131131
className="capitalize ltr:ml-2 rtl:mr-2"
132132
color="secondary"
133-
onClick={() => setSelectedDate(today.format("YYYY-MM-DD"))}>
133+
onClick={() => setSelectedDate({ date: today.format("YYYY-MM-DD") })}>
134134
{t("today")}
135135
</Button>
136136
)}

packages/features/bookings/Booker/store.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ export type BookerStore = {
8383
* Date selected by user (exact day). Format is YYYY-MM-DD.
8484
*/
8585
selectedDate: string | null;
86-
setSelectedDate: (date: string | null, omitUpdatingParams?: boolean) => void;
86+
setSelectedDate: (params: {
87+
date: string | null;
88+
omitUpdatingParams?: boolean;
89+
preventMonthSwitching?: boolean;
90+
}) => void;
8791
addToSelectedDate: (days: number) => void;
8892
/**
8993
* Multiple Selected Dates and Times
@@ -192,7 +196,7 @@ export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({
192196
return set({ layout });
193197
},
194198
selectedDate: getQueryParam("date") || null,
195-
setSelectedDate: (selectedDate: string | null, omitUpdatingParams = false) => {
199+
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
196200
// unset selected date
197201
if (!selectedDate) {
198202
removeQueryParam("date");
@@ -207,7 +211,8 @@ export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({
207211
}
208212

209213
// Setting month make sure small calendar in fullscreen layouts also updates.
210-
if (newSelection.month() !== currentSelection.month()) {
214+
// preventMonthSwitching is true in monthly view
215+
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) {
211216
set({ month: newSelection.format("YYYY-MM") });
212217
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
213218
updateQueryParam("month", newSelection.format("YYYY-MM"));
@@ -264,7 +269,7 @@ export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({
264269
if (!get().isPlatform || get().allowUpdatingUrlParams) {
265270
updateQueryParam("month", month ?? "");
266271
}
267-
get().setSelectedDate(null);
272+
get().setSelectedDate({ date: null });
268273
},
269274
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
270275
setDayCount: (dayCount: number | null) => {

packages/features/calendars/DatePicker.tsx

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { PeriodData } from "@calcom/types/Event";
1414
import classNames from "@calcom/ui/classNames";
1515
import { Button } from "@calcom/ui/components/button";
1616
import { SkeletonText } from "@calcom/ui/components/skeleton";
17+
import { Tooltip } from "@calcom/ui/components/tooltip";
1718

1819
import NoAvailabilityDialog from "./NoAvailabilityDialog";
1920

@@ -56,6 +57,8 @@ export type DatePickerProps = {
5657
}[]
5758
>;
5859
periodData?: PeriodData;
60+
// Whether this is a compact sidebar view or main monthly view
61+
isCompact?: boolean;
5962
};
6063

6164
const Day = ({
@@ -65,6 +68,8 @@ const Day = ({
6568
away,
6669
emoji,
6770
customClassName,
71+
showMonthTooltip,
72+
isFirstDayOfNextMonth,
6873
...props
6974
}: JSX.IntrinsicElements["button"] & {
7075
active: boolean;
@@ -75,12 +80,14 @@ const Day = ({
7580
dayContainer?: string;
7681
dayActive?: string;
7782
};
83+
showMonthTooltip?: boolean;
84+
isFirstDayOfNextMonth?: boolean;
7885
}) => {
7986
const { t } = useLocale();
8087
const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton");
8188
const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton");
8289

83-
return (
90+
const buttonContent = (
8491
<button
8592
type="button"
8693
style={disabled ? { ...disabledDateButtonEmbedStyles } : { ...enabledDateButtonEmbedStyles }}
@@ -113,6 +120,33 @@ const Day = ({
113120
)}
114121
</button>
115122
);
123+
124+
const content = showMonthTooltip ? (
125+
<Tooltip content={date.format("MMMM")}>{buttonContent}</Tooltip>
126+
) : (
127+
buttonContent
128+
);
129+
130+
return (
131+
<>
132+
{isFirstDayOfNextMonth && (
133+
<div
134+
className={classNames(
135+
"absolute top-0 z-10 mx-auto w-fit rounded-full font-semibold uppercase tracking-wide",
136+
active ? "text-white" : "text-default",
137+
disabled && "bg-emphasis"
138+
)}
139+
style={{
140+
fontSize: "10px",
141+
lineHeight: "13px",
142+
padding: disabled ? "0 3px" : "3px 3px 3px 4px",
143+
}}>
144+
{date.format("MMM")}
145+
</div>
146+
)}
147+
{content}
148+
</>
149+
);
116150
};
117151

118152
const Days = ({
@@ -129,6 +163,7 @@ const Days = ({
129163
customClassName,
130164
isBookingInPast,
131165
periodData,
166+
isCompact,
132167
...props
133168
}: Omit<DatePickerProps, "locale" | "className" | "weekStart"> & {
134169
DayComponent?: React.FC<React.ComponentProps<typeof Day>>;
@@ -143,20 +178,48 @@ const Days = ({
143178
scrollToTimeSlots?: () => void;
144179
isBookingInPast: boolean;
145180
periodData: PeriodData;
181+
isCompact?: boolean;
146182
}) => {
147-
// Create placeholder elements for empty days in first week
148-
const weekdayOfFirst = browsingDate.date(1).day();
149-
150183
const includedDates = getAvailableDatesInMonth({
151184
browsingDate: browsingDate.toDate(),
152185
minDate,
153186
includedDates: props.includedDates,
154187
});
155188

156-
const days: (Dayjs | null)[] = Array((weekdayOfFirst - weekStart + 7) % 7).fill(null);
157-
for (let day = 1, dayCount = daysInMonth(browsingDate); day <= dayCount; day++) {
158-
const date = browsingDate.set("date", day);
159-
days.push(date);
189+
const today = dayjs();
190+
const firstDayOfMonth = browsingDate.startOf("month");
191+
const isSecondWeekOver = today.isAfter(firstDayOfMonth.add(2, "week"));
192+
let days: (Dayjs | null)[] = [];
193+
194+
const getPadding = (day: number) => (browsingDate.set("date", day).day() - weekStart + 7) % 7;
195+
const totalDays = daysInMonth(browsingDate);
196+
197+
// Only apply end-of-month logic for main monthly view (not compact sidebar)
198+
if (isSecondWeekOver && !isCompact) {
199+
const startDay = 8;
200+
const pad = getPadding(startDay);
201+
days = Array(pad).fill(null);
202+
203+
for (let day = startDay; day <= totalDays; day++) {
204+
days.push(browsingDate.set("date", day));
205+
}
206+
207+
const remainingInRow = days.length % 7;
208+
const extraDays = (remainingInRow > 0 ? 7 - remainingInRow : 0) + 7;
209+
const nextMonth = browsingDate.add(1, "month");
210+
211+
// Add days starting from day 1 of next month
212+
for (let i = 0; i < extraDays; i++) {
213+
days.push(nextMonth.set("date", 1 + i));
214+
}
215+
} else {
216+
// Traditional calendar grid logic for compact sidebar or early in month
217+
const pad = getPadding(1);
218+
days = Array(pad).fill(null);
219+
220+
for (let day = 1; day <= totalDays; day++) {
221+
days.push(browsingDate.set("date", day));
222+
}
160223
}
161224

162225
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
@@ -188,20 +251,29 @@ const Days = ({
188251

189252
const daysToRenderForTheMonth = days.map((day) => {
190253
if (!day) return { day: null, disabled: true };
254+
191255
const dateKey = yyyymmdd(day);
192-
const oooInfo = slots && slots?.[dateKey] ? slots?.[dateKey]?.find((slot) => slot.away) : null;
256+
const daySlots = slots?.[dateKey] || [];
257+
const oooInfo = daySlots.find((slot) => slot.away) || null;
258+
259+
const isNextMonth = day.month() !== browsingDate.month();
260+
const isFirstDayOfNextMonth = isSecondWeekOver && !isCompact && isNextMonth && day.date() === 1;
261+
193262
const included = includedDates?.includes(dateKey);
194263
const excluded = excludedDates.includes(dateKey);
195264

196-
const isOOOAllDay = !!(slots && slots[dateKey] && slots[dateKey].every((slot) => slot.away));
265+
const hasAvailableSlots = daySlots.some((slot) => !slot.away);
266+
const isOOOAllDay = daySlots.length > 0 && daySlots.every((slot) => slot.away);
197267
const away = isOOOAllDay;
198-
const disabled = away ? !oooInfo?.toUser : !included || excluded;
268+
269+
const disabled = away ? !oooInfo?.toUser : isNextMonth ? !hasAvailableSlots : !included || excluded;
199270

200271
return {
201-
day: day,
272+
day,
202273
disabled,
203274
away,
204275
emoji: oooInfo?.emoji,
276+
isFirstDayOfNextMonth,
205277
};
206278
});
207279

@@ -239,7 +311,7 @@ const Days = ({
239311

240312
return (
241313
<>
242-
{daysToRenderForTheMonth.map(({ day, disabled, away, emoji }, idx) => (
314+
{daysToRenderForTheMonth.map(({ day, disabled, away, emoji, isFirstDayOfNextMonth }, idx) => (
243315
<div key={day === null ? `e-${idx}` : `day-${day.format()}`} className="relative w-full pt-[100%]">
244316
{day === null ? (
245317
<div key={`e-${idx}`} />
@@ -265,6 +337,8 @@ const Days = ({
265337
active={isActive(day)}
266338
away={away}
267339
emoji={emoji}
340+
showMonthTooltip={isSecondWeekOver && !isCompact}
341+
isFirstDayOfNextMonth={isFirstDayOfNextMonth}
268342
/>
269343
)}
270344
</div>
@@ -297,6 +371,7 @@ const DatePicker = ({
297371
periodDays: null,
298372
periodType: "UNLIMITED",
299373
},
374+
isCompact,
300375
...passThroughProps
301376
}: DatePickerProps &
302377
Partial<React.ComponentProps<typeof Days>> & {
@@ -406,6 +481,7 @@ const DatePicker = ({
406481
includedDates={includedDates}
407482
isBookingInPast={isBookingInPast}
408483
periodData={periodData}
484+
isCompact={isCompact}
409485
/>
410486
</div>
411487
</div>

0 commit comments

Comments
 (0)