Files
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>sean-brydon
52d5261b51 feat(booking): implement cross-page/week navigation in booking details sheet with view persistence (#25545)
* display UTM parameters

* persist view (list | calendar) in localStorage

* move to next page on clicking "next" of last item in the page

improve navigation

^ Conflicts:
^	apps/web/modules/bookings/components/BookingCalendarContainer.tsx

* fix navigation on calendar view

* Delete apps/web/modules/bookings/NAVIGATION_IMPLEMENTATION.md

* avoid page size being 0

* check feature flag on user level

* use map to improve useBookingListData

* address feedback

* feat(bookings): add smart navigation for calendar view

- Add sort option to tRPC bookings.get schema and handler
- Create NAVIGATION_PROBE_WINDOW_MONTHS constant (3 months)
- Create useNearestFutureBooking hook to find nearest future booking
- Create useNearestPastBooking hook to find nearest past booking
- Update useCalendarNavigationCapabilities to use probe results
- Update BookingCalendarContainer to wire up probe hooks

This enables the booking details sheet to:
- Disable next/prev buttons when no bookings exist in that direction
- Jump directly to the week containing the nearest booking

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

* fix type error

* feat: add booking selection state when using slideover (#25637)

* implement booking selection when using slideover

* remove pixel shift

* fix

* auto scroll to selected event on calendar

* make DateValues header sticky when scrolling

---------

Co-authored-by: Eunjae Lee <[email protected]>

* prefetch previous / next weeks on calendar view

* clean up classes

* handle "fetched & but no data" situation more correctly in useCalendarAutoSelector

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <[email protected]>
2025-12-15 16:04:00 +00:00

106 lines
3.6 KiB
TypeScript

import React from "react";
import { createStore, useStore } from "zustand";
import type { StoreApi } from "zustand";
import dayjs from "@calcom/dayjs";
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
import type {
CalendarComponentProps,
CalendarPublicActions,
CalendarState,
CalendarStoreProps,
} from "../types/state";
import { mergeOverlappingDateRanges, weekdayDates } from "../utils";
const defaultState: CalendarComponentProps = {
view: "week",
startDate: weekdayDates(0, new Date()).startDate,
endDate: weekdayDates(0, new Date()).endDate,
events: [],
startHour: 0,
endHour: 23,
gridCellsPerHour: 4,
timezone: CURRENT_TIMEZONE,
showBackgroundPattern: true,
showBorder: true,
borderColor: "default",
showTimezone: false,
};
export function createCalendarStore(initial?: Partial<CalendarComponentProps>): StoreApi<CalendarStoreProps> {
return createStore<CalendarStoreProps>((set) => ({
...defaultState,
...initial,
setView: (view: CalendarComponentProps["view"]) => set({ view }),
setStartDate: (startDate: CalendarComponentProps["startDate"]) => set({ startDate }),
setEndDate: (endDate: CalendarComponentProps["endDate"]) => set({ endDate }),
setEvents: (events: CalendarComponentProps["events"]) => set({ events }),
// This looks a bit odd but init state only overrides the public props + actions as we don't want to override our internal state
initState: (state: CalendarState & CalendarPublicActions) => {
// Handle sorting of events if required
let events = state.events;
if (state.sortEvents) {
events = [...state.events].sort((a, b) => dayjs(a.start).valueOf() - dayjs(b.start).valueOf());
}
const blockingDates = mergeOverlappingDateRanges(state.blockingDates || []); // We merge overlapping dates so we don't get duplicate blocking "Cells" in the UI
set({
...state,
blockingDates,
events,
selectedBookingUid: state.selectedBookingUid,
});
},
setSelectedEvent: (event) => set({ selectedEvent: event }),
handleDateChange: (payload) =>
set((state) => {
const { startDate, endDate } = state;
if (payload === "INCREMENT") {
const newStartDate = dayjs(startDate).add(1, state.view).toDate();
const newEndDate = dayjs(endDate).add(1, state.view).toDate();
// Do nothing if
if (
(state.minDate && newStartDate < state.minDate) ||
(state.maxDate && newEndDate > state.maxDate)
) {
return {
startDate,
endDate,
};
}
// We call this callback if we have it -> Allows you to change your state outside of the component
state.onDateChange?.(newStartDate, newEndDate);
return {
startDate: newStartDate,
endDate: newEndDate,
};
}
const newStartDate = dayjs(startDate).subtract(1, state.view).toDate();
const newEndDate = dayjs(endDate).subtract(1, state.view).toDate();
state.onDateChange?.(newStartDate, newEndDate);
return {
startDate: newStartDate,
endDate: newEndDate,
};
}),
}));
}
export const CalendarStoreContext = React.createContext<StoreApi<CalendarStoreProps> | null>(null);
export function useCalendarStore<T>(
selector: (state: CalendarStoreProps) => T,
equalityFn?: (a: T, b: T) => boolean
): T {
const store = React.useContext(CalendarStoreContext);
if (!store) {
throw new Error("useCalendarStore must be used within a CalendarStoreProvider");
}
return useStore(store, selector, equalityFn);
}