UNPKG

events-calendar

Version:

A flexible, responsive, and feature-rich calendar component for modern React applications.

1 lines 148 kB
{"version":3,"sources":["../package/EventsCalendar.tsx","../package/components/event/Event.tsx","../package/components/event/utils/getClipPath.ts","../package/components/event/utils/getEventStyles.ts","../package/components/event/utils/isBeingDragged.ts","../package/components/event/utils/getOverflowArrows.ts","../package/utils/setTime.ts","../package/utils/constants.ts","../package/utils/isBetween.ts","../package/utils/isSameOrBefore.ts","../package/utils/hasOverlap.ts","../package/utils/updateEvent.ts","../package/utils/getTimeDiff.ts","../package/utils/getTimeLabel.ts","../package/utils/filterByWeek.ts","../package/utils/filterByDate.ts","../package/utils/filterByView.ts","../package/utils/parseRawEvents.ts","../package/utils/getVisibleEvents.ts","../package/utils/arrangeWeekEvents.ts","../package/utils/returnValidStartEnd.ts","../package/state/EMPTY_EVENT.tsx","../package/utils/getPlaceholderEvent.ts","../package/utils/arrangeWeekdayEvents.ts","../package/utils/createDayjsObjFromTime.ts","../package/utils/getBackgroundFromGroups.ts","../package/components/event/utils/getWeekOrDayEventStyles.ts","../package/components/event/AllDayEvent.tsx","../package/components/event/TimeEvent.tsx","../package/hooks/useMouseEvent.tsx","../package/hooks/useElementSize.tsx","../package/hooks/useCalendarEvent.tsx","../package/hooks/useEventsCalendar.tsx","../package/state/DEFAULT_STATE.tsx","../package/state/reducer.tsx","../package/hooks/useInitEventsCalendar.tsx","../package/components/popover/PopoverContent.tsx","../package/components/popover/useBindPopover.tsx","../package/components/popover/EventsCalendarPopover.tsx","../package/components/event-cell/CellHeader.tsx","../package/components/event-cell/ShowMoreText.tsx","../package/components/event-cell/CellContainer.tsx","../package/components/circular-loader/CircularLoader.tsx","../package/features/header/HeaderButton.tsx","../package/features/header/Controls.tsx","../package/features/header/Navigation.tsx","../package/features/icons/IconChevronLeft.tsx","../package/features/icons/IconChevronRight.tsx","../package/features/header/Header.tsx","../package/features/time-view/TimeView.tsx","../package/features/time-view/components/HoursColumn.tsx","../package/features/time-view/components/TimeIndicator.tsx","../package/features/time-view/components/TimeBackground.tsx","../package/features/time-view/components/TimeViewHeader.tsx","../package/features/time-view/hooks/useSetInitialScroll.tsx","../package/features/time-view/utils/getWeekDates.ts","../package/features/time-view/utils/getEventsByDayMap.ts","../package/features/year-view/components/MonthDate.tsx","../package/features/year-view/utils/generateYearDates.ts","../package/features/year-view/YearView.tsx","../package/features/month-view/MonthView.tsx","../package/features/month-view/components/MonthHeader.tsx","../package/features/month-view/utils/getMaxEvents.ts","../package/features/month-view/utils/getMonthDates.ts","../package/features/month-view/utils/getEventsByWeekMap.ts","../package/features/overflow-card/OverflowCard.tsx"],"sourcesContent":["'use client';\n\nimport { ReactNode, useMemo, useRef } from 'react';\n\nimport './EventsCalendar.css';\n\nimport { CircularLoader, EventsCalendarPopover } from './components';\nimport { Header, MonthView, OverflowCard, TimeView, YearView } from './features';\nimport { EventsCalendarObject, useInitEventsCalendar, useMouseEvent } from './hooks';\nimport {\n CalendarView,\n EventClickArgs,\n EventEditProps,\n EventsCalendarContextMenuProps,\n EventsCalendarPopoverProps,\n RawCalendarEvent,\n RawCalendarEventBase,\n} from './types';\nimport { CALENDAR_VIEWS, filterByView, parseRawEvents } from './utils';\n\nexport interface EventsCalendarProps<T extends RawCalendarEventBase = RawCalendarEventBase> {\n /**\n * Shared calendar state and methods, returned by `useEventsCalendar`.\n */\n calendar?: EventsCalendarObject;\n\n /**\n * Renders a more compact layout, useful in constrained spaces.\n * Affects padding, font size, and layout density.\n */\n compact?: boolean;\n\n /**\n * Enables drag-to-create behavior for new events.\n */\n enableDragCreation?: boolean;\n\n /**\n * Allows users to reschedule events by dragging.\n */\n enableRescheduling?: boolean;\n\n /**\n * Array of event objects to be rendered in the calendar.\n */\n events?: RawCalendarEvent<T>[];\n\n /**\n * Whether to show a loading spinner overlay on the calendar.\n */\n isFetching?: boolean;\n\n /**\n * If true, hides the built-in calendar header (navigation + view toggle).\n */\n noHeader?: boolean;\n\n /**\n * z-index used for the overflow popover that appears when clicking \"+N more\".\n */\n overflowPopoverZIndex?: number;\n\n /**\n * z-index used for the event popover component.\n */\n popoverZIndex?: number;\n\n /**\n * Calendar views available to the user (e.g. 'month', 'week', 'day', 'year').\n * Defaults to all supported views.\n */\n views?: CalendarView[];\n\n /**\n * Fired when an existing event is clicked.\n */\n onEventClick?: (props: EventClickArgs<T>) => void;\n\n /**\n * Fired when a new event is created (e.g. via drag or click).\n */\n onEventCreate?: (props: EventEditProps) => void;\n\n /**\n * Fired when an event is rescheduled (e.g. via drag).\n */\n onEventReschedule?: (props: EventEditProps) => void;\n\n /**\n * Custom render function for the event popover.\n */\n renderPopover?: (props: EventsCalendarPopoverProps) => ReactNode;\n\n /**\n * Custom render function for a context menu (e.g. right-click on event).\n */\n renderContextMenu?: (props: EventsCalendarContextMenuProps) => ReactNode;\n}\n\nexport function EventsCalendar<T extends RawCalendarEvent = RawCalendarEventBase>({\n calendar,\n compact = false,\n enableDragCreation = false,\n enableRescheduling = false,\n events = [],\n overflowPopoverZIndex = 2,\n popoverZIndex = 101,\n isFetching = false,\n noHeader = false,\n onEventClick,\n onEventCreate,\n onEventReschedule,\n renderPopover,\n renderContextMenu,\n views = [...CALENDAR_VIEWS],\n}: EventsCalendarProps<T>) {\n // Initialise data calendar\n const { activeDate, setActiveDate, view, setView, state, dispatch } =\n useInitEventsCalendar(calendar);\n\n // Parse raw events\n const parsed = useMemo(() => parseRawEvents<T>(events), [events]);\n\n // Events in current view\n const calendarEvents = useMemo(\n () => filterByView<T>(parsed, activeDate, view),\n [parsed, activeDate, view]\n );\n\n // Overflow events (only all day events are needed in time views)\n const isTimeView = view === 'week' || view === 'day';\n const overflowEvents = isTimeView\n ? calendarEvents.filter((event) => event.isAllDay)\n : calendarEvents;\n\n // Calendar state\n const {\n eventAnchor,\n dragActive,\n eventDragActive,\n popoverIsOpen,\n clickedEvent,\n placeholderEvent,\n } = state;\n\n // Popover handlers\n const onClose = () => dispatch({ type: 'reset_calendar' });\n const handleStopDrag = () => {\n if (dragActive || eventDragActive) dispatch({ type: 'event_create_stop' });\n };\n\n // Placeholder ref\n const placeholderRef = useRef<HTMLDivElement>(null);\n\n // Mouse event handler\n const handleMouseEvent = useMouseEvent({ enableDragCreation, dispatch, state, onEventCreate });\n\n // Shared props\n const basicProps = {\n activeDate,\n dispatch,\n state,\n events: calendarEvents,\n };\n const sharedViewProps = {\n ...basicProps,\n compact,\n onEventClick,\n handleStopDrag,\n placeholderRef,\n handleMouseEvent,\n onEventReschedule,\n renderContextMenu,\n enableRescheduling,\n };\n\n // Render function\n const renderCurrentView = () => {\n switch (view) {\n case 'year':\n return <YearView {...basicProps} />;\n case 'month':\n return <MonthView {...sharedViewProps} />;\n case 'week':\n case 'day':\n return <TimeView {...sharedViewProps} view={view} />;\n default:\n return null;\n }\n };\n\n return (\n <div className=\"events-calendar-wrapper\">\n {noHeader ? null : (\n <Header\n view={view}\n setActiveDate={setActiveDate}\n setView={setView}\n activeDate={activeDate}\n views={views}\n />\n )}\n <div\n className=\"events-calendar\"\n data-withheader={!noHeader}\n onClick={(e) => e.stopPropagation()}\n >\n <CircularLoader visible={isFetching} />\n {renderCurrentView()}\n\n {renderPopover && eventAnchor && (\n <EventsCalendarPopover isOpen={popoverIsOpen} anchor={eventAnchor} zIndex={popoverZIndex}>\n {renderPopover({ onClose, clickedEvent, newEvent: placeholderEvent })}\n </EventsCalendarPopover>\n )}\n\n <OverflowCard\n state={state}\n compact={compact}\n dispatch={dispatch}\n events={overflowEvents}\n onEventClick={onEventClick}\n placeholderRef={placeholderRef}\n renderContextMenu={renderContextMenu}\n enableRescheduling={enableRescheduling}\n zIndex={overflowPopoverZIndex}\n />\n </div>\n </div>\n );\n}\n","'use client';\n\nimport { Dayjs } from 'dayjs';\nimport { Dispatch, ReactNode, RefObject, useRef } from 'react';\n\nimport './Event.css';\n\nimport { AllDayEvent } from './AllDayEvent';\nimport { TimeEvent } from './TimeEvent';\nimport { getEventStyles, getWeekOrDayEventStyles, isBeingDragged } from './utils';\nimport { useCalendarEvent } from '~/hooks';\nimport {\n CalendarAction,\n CalendarState,\n CalendarView,\n EventClickArgs,\n EventsCalendarContextMenuProps,\n MinMaxDatesInView,\n OrderedCalendarEvent,\n} from '~/types';\nimport { getTimeDiff, getTimeLabel } from '~/utils';\n\ninterface EventProps<T> {\n view: CalendarView;\n enableRescheduling: boolean;\n compact?: boolean;\n date: Dayjs;\n dispatch: Dispatch<CalendarAction>;\n event: OrderedCalendarEvent<T>;\n isInWeekHeader?: boolean;\n isInOverflow?: boolean;\n isInDayHeader?: boolean;\n minMaxDatesInView?: MinMaxDatesInView;\n placeholderRef: RefObject<HTMLDivElement | null>;\n onEventClick?: (props: EventClickArgs<T>) => void;\n renderContextMenu?: (props: EventsCalendarContextMenuProps) => ReactNode;\n state: CalendarState;\n}\n\nexport function Event<T>({\n view,\n enableRescheduling,\n compact = false,\n date,\n dispatch,\n event,\n isInWeekHeader = false,\n isInOverflow = false,\n isInDayHeader = false,\n minMaxDatesInView,\n onEventClick,\n placeholderRef,\n renderContextMenu,\n state,\n}: EventProps<T>) {\n const ref = useRef<HTMLDivElement>(null);\n const hasContextMenu = !!renderContextMenu;\n const isInteractive = !!onEventClick || enableRescheduling;\n const isMonthView = view === 'month';\n const isDayView = view === 'day';\n\n // Popover handlers\n const openPopover = () => dispatch({ type: 'open_popover' });\n const closePopover = () => dispatch({ type: 'reset_calendar' });\n\n // Context menu onClose\n const onClose = () => dispatch({ type: 'reset_calendar' });\n\n // Handle event left click (view popover)\n const handleClick = (\n e: React.MouseEvent<HTMLDivElement, MouseEvent>,\n isPlaceholder: boolean,\n eventRef: RefObject<HTMLDivElement | null>,\n onEventClick?: (props: EventClickArgs<T>) => void\n ) => {\n e.stopPropagation();\n if (isPlaceholder) return;\n\n if (!eventRef.current) return;\n\n // Check if popover is open and anchored to the clicked event, we want to\n // move the popover if the event is clicked twice, but on different weeks)\n const isDoubleClick =\n isActive && state.eventAnchor?.dataset.anchorday === eventRef.current.dataset.anchorday;\n\n // Overflow popover should close if clicked event was not inside the overflow popover\n const overflowShouldClose = !isInOverflow && state.overflowIsOpen;\n\n // Close popover (this is either a double-click on an open event or an event outside the popover)\n if (overflowShouldClose) dispatch({ type: 'reset_calendar' });\n\n // Open popover if there is currently no anchored popover\n if (!isDoubleClick) {\n dispatch({ type: 'set_clicked_event', event: event, anchor: eventRef.current });\n }\n\n // Toggle popover visibility handler\n const togglePopover = () => (isDoubleClick ? closePopover() : openPopover());\n\n onEventClick?.({\n event,\n isDoubleClick,\n eventRef: eventRef.current,\n openPopover,\n closePopover,\n togglePopover,\n });\n };\n\n // Main event hook\n const {\n handleContextMenu,\n isActive,\n contextIsOpen,\n refs,\n floatingStyles,\n getFloatingProps,\n closeContextMenu,\n } = useCalendarEvent({\n dispatch,\n event,\n state,\n hasContextMenu,\n isInOverflow,\n });\n\n // Current event is placeholder event\n const isPlaceholder = event.id === null;\n const eventRef = isPlaceholder && event.start.isSame(date, 'd') ? placeholderRef : ref;\n const timeDuration = Math.abs(getTimeDiff(event.start, event.end));\n const isShort = timeDuration <= 30;\n const overlapOffset = isDayView ? 120 : 20;\n const dayIndex = isDayView ? 0 : date.day();\n\n const onDragStart = () => {\n dispatch({ type: 'reset_calendar' });\n if (enableRescheduling && !isInOverflow) {\n dispatch({\n type: 'event_reschedule_start',\n event: { ...event, dragId: event.id, order: isMonthView ? event.order : 1000 },\n });\n }\n };\n\n const styles = isMonthView\n ? getEventStyles(isInOverflow, event, date, compact, isInWeekHeader, isInDayHeader)\n : getWeekOrDayEventStyles(\n event,\n timeDuration,\n dayIndex,\n overlapOffset,\n isInteractive && isActive\n );\n\n return (\n <>\n <div\n className={\n isMonthView\n ? 'events-calendar-month-item-container'\n : 'events-calendar-week-item-container'\n }\n data-interactive={isInteractive}\n data-active={isInteractive && isActive}\n data-anchorday={date.format('DD-MMM-YYYY')}\n data-placeholder={isPlaceholder}\n data-dragactive={state.dragActive || state.eventDragActive}\n data-isdragging={isBeingDragged(state, event)}\n data-sm={compact}\n data-time={isMonthView && !event.isAllDay}\n draggable=\"true\"\n onClick={(e) => handleClick(e, isPlaceholder, eventRef, onEventClick)}\n onContextMenu={(e) => handleContextMenu(e, eventRef)}\n onDragStart={onDragStart}\n ref={eventRef}\n style={styles}\n >\n {isMonthView ? (\n !event.isAllDay ? (\n <TimeEvent event={event} isCompact={compact} />\n ) : (\n <AllDayEvent\n date={date}\n event={event}\n isCompact={compact}\n isInOverflow={isInOverflow}\n minMaxDatesInView={minMaxDatesInView}\n />\n )\n ) : (\n <div className=\"events-calendar-item-label\" data-short={isShort}>\n <span style={isShort ? undefined : { width: '100%' }}>{event.title}</span>\n <span className=\"events-calendar-time-text\">{getTimeLabel(event, timeDuration)}</span>\n </div>\n )}\n </div>\n {contextIsOpen && (\n <div\n ref={refs.setFloating}\n style={{ ...floatingStyles, zIndex: 2 }}\n {...getFloatingProps()}\n >\n {renderContextMenu &&\n renderContextMenu({ event, closeContextMenu, onClose, openPopover })}\n </div>\n )}\n </>\n );\n}\n","import { OverflowArrowType } from '~/types';\n\nexport const getClipPath = (overflowArrows: OverflowArrowType): string | undefined => {\n switch (overflowArrows) {\n case 'both':\n return 'polygon(10px 0%, calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 10px 100%, 0% 50%)';\n case 'left':\n return 'polygon(10px 0%, 100% 0%, 100% 100%, 10px 100%, 0% 50%)';\n case 'right':\n return 'polygon(0% 0%, calc(100% - 10px) 0%, 100% 50%, calc(100% - 10px) 100%, 0% 100%)';\n case 'none':\n return undefined;\n default:\n return undefined;\n }\n};\n","import { Dayjs } from 'dayjs';\nimport { CSSProperties } from 'react';\nimport { OrderedCalendarEvent } from '~/types';\n\nexport const getEventStyles = (\n isInOverflow: boolean,\n event: OrderedCalendarEvent,\n date: Dayjs,\n isCompact: boolean,\n isInWeekHeader: boolean,\n isInDayHeader: boolean\n) => {\n if (isInOverflow) return { width: '100%' };\n const weekStart = event.start.isSame(date, 'w') ? event.start.day() : 0;\n const weekEnd = event.end.isSame(date, 'w') ? event.end.day() : 6;\n const styles: CSSProperties = {\n position: 'absolute',\n left: isInDayHeader ? '0.125rem' : `calc(${(weekStart * 100) / 7}% + 0.125rem)`,\n top: `calc(${isInDayHeader ? 4 : isCompact ? 24 : 26}px + ${event.order * (isCompact ? 21 : 22)}px + ${\n isInWeekHeader ? 6 : 0\n }px)`,\n width: isInDayHeader\n ? 'calc(100% - 6px)'\n : `calc(${(100 * (1 + (weekEnd - weekStart))) / 7}% - 0.25rem - 1px)`,\n };\n return styles;\n};\n","import { CalendarEvent, CalendarState } from '~/types';\n\nexport function isBeingDragged(state: CalendarState, event: CalendarEvent) {\n return !event.isActive && event.id === state.placeholderEvent.dragId;\n}\n","import dayjs from 'dayjs';\nimport { MinMaxDatesInView, OverflowArrowType } from '~/types';\n\nexport const getOverflowArrows = (\n isInOverflow: boolean,\n date: dayjs.Dayjs,\n start: dayjs.Dayjs,\n end: dayjs.Dayjs,\n minMaxDatesInView?: MinMaxDatesInView\n): OverflowArrowType => {\n const showLeftArrow = isInOverflow\n ? start.isBefore(date, 'day')\n : !!minMaxDatesInView?.first && start.isBefore(minMaxDatesInView.first, 'day');\n\n const showRightArrow = isInOverflow\n ? end.isAfter(date, 'day')\n : !!minMaxDatesInView?.last && end.isAfter(minMaxDatesInView.last, 'day');\n\n return showLeftArrow && showRightArrow\n ? 'both'\n : showLeftArrow\n ? 'left'\n : showRightArrow\n ? 'right'\n : 'none';\n};\n","import { Dayjs } from 'dayjs';\n\n/*\n Function that takes in a date object and a time\n Returns date object with same date, but resets time to input\n*/\nexport function setTime(date: Dayjs, time: Dayjs) {\n return date.hour(time.hour()).minute(time.minute());\n}\n","export const CALENDAR_VIEWS = ['year', 'month', 'week', 'day'] as const;\n\nexport const MONTHS = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n] as const;\n\nexport const DAY_LABELS_SHORT = ['S', 'M', 'T', 'W', 'T', 'F', 'S'] as const;\n\nexport const DAYS_IN_FULL_MONTH = 42;\n\nexport const DEFAULT_COLOR = '#0ea5e9';\n\nexport const HOURS = [\n '1 AM',\n '2 AM',\n '3 AM',\n '4 AM',\n '5 AM',\n '6 AM',\n '7 AM',\n '8 AM',\n '9 AM',\n '10 AM',\n '11 AM',\n '12 PM',\n '1 PM',\n '2 PM',\n '3 PM',\n '4 PM',\n '5 PM',\n '6 PM',\n '7 PM',\n '8 PM',\n '9 PM',\n '10 PM',\n '11 PM',\n] as const;\n","import { Dayjs } from 'dayjs';\n\n/**\n * Checks if a date is between two other dates, inclusive of the start date,\n * and optionally exclusive of the end date.\n *\n * @param date - The date to test.\n * @param testStart - The start date of the range.\n * @param testEnd - The end date of the range.\n * @param startOnly - If true, only consider the start date as inclusive (default: false).\n * @returns True if `date` is between `testStart` and `testEnd` (inclusive start, optional inclusive end).\n */\nexport function isBetween(\n date: Dayjs,\n testStart: Dayjs,\n testEnd: Dayjs,\n startOnly: boolean = false\n): boolean {\n if (date.isSame(testStart, 'd')) return true;\n if (!startOnly && date.isSame(testEnd, 'd')) return true;\n return date.isAfter(testStart) && date.isBefore(testEnd);\n}\n","import { Dayjs } from 'dayjs';\n\n/**\n * Checks if a date is the same as or before another date, using day precision.\n *\n * @param a - The first Dayjs date\n * @param b - The second Dayjs date\n * @returns `true` if `a` is the same as or before `b`, otherwise `false`\n */\nexport function isSameOrBefore(a: Dayjs, b: Dayjs): boolean {\n return a.isBefore(b, 'd') || a.isSame(b, 'd');\n}\n","import { Dayjs } from 'dayjs';\nimport { isSameOrBefore } from './isSameOrBefore';\n\n/**\n * Checks if two date ranges overlap.\n *\n * Returns true if any part of the first range (start1 to end1)\n * overlaps with the second range (start2 to end2).\n *\n * @param start1 - Start of the first date range\n * @param end1 - End of the first date range\n * @param start2 - Start of the second date range\n * @param end2 - End of the second date range\n * @returns `true` if the two date ranges overlap, otherwise `false`\n */\n\nexport function hasOverlap(start1: Dayjs, end1: Dayjs, start2: Dayjs, end2: Dayjs) {\n // return (\n // \tisBetween(start1, start2, end2) || // range 1 starts within range 2\n // \tisBetween(start2, start1, end1) || // range 2 starts within range 1\n // \tisBetween(end2, start1, end1) // range 2 ends within range 1\n // );\n\n return isSameOrBefore(start1, end2) && isSameOrBefore(start2, end1);\n}\n","import { Dayjs } from 'dayjs';\nimport { Dispatch } from 'react';\nimport { CalendarAction, CalendarState, CalendarView } from '~/types';\n\ninterface updateDragNDropPlaceholderProps {\n state: CalendarState;\n dispatch: Dispatch<CalendarAction>;\n date: Dayjs;\n view: CalendarView;\n}\n\nexport function updateEvent({ state, dispatch, date, view }: updateDragNDropPlaceholderProps) {\n const timeScale = view === 'month' ? 'day' : 'minute';\n\n // After placeholder event is created (for dragging), record where on the event we have clicked (e.g. 2 days/60mins in)\n if (state.dragStartOffset === null) {\n const dateDiff = date.diff(state.clickedEvent.start, timeScale);\n dispatch({ type: 'update_event', dragStartOffset: dateDiff });\n return;\n }\n\n // Extract existing values\n const { start, end, startTime, endTime } = state.clickedEvent;\n\n // On drag over grid, update the placeholder event\n const newStart = date.subtract(state.dragStartOffset, timeScale);\n const increment = end.diff(start, timeScale);\n const newEnd = newStart.add(increment, timeScale);\n\n if (view !== 'month' && !newStart.isSame(newEnd, 'day')) return;\n\n dispatch({\n type: 'update_event',\n event: {\n ...state.placeholderEvent,\n start: view === 'month' ? newStart.hour(start.hour()).minute(start.minute()) : newStart,\n end: view === 'month' ? newEnd.hour(end.hour()).minute(end.minute()) : newEnd,\n startTime: view === 'month' ? startTime : newStart.format('h:mma'),\n endTime: view === 'month' ? endTime : newEnd.format('h:mma'),\n },\n });\n}\n","import { Dayjs } from 'dayjs';\n\nexport function getTimeDiff(dt1: Dayjs, dt2: Dayjs) {\n const mins1 = dt1.hour() * 60 + dt1.minute();\n const mins2 = dt2.hour() * 60 + dt2.minute();\n return mins2 - mins1;\n}\n","import { CalendarEvent } from '~/types';\n\nexport function getTimeLabel(event: CalendarEvent, timeDuration: number) {\n if (!event.start || !event.end) return '';\n const leadingComma = timeDuration <= 30 ? ', ' : '';\n return event.start.isBefore(event.end)\n ? `${leadingComma}${event.start.format('h:mma')} - ${event.end.format('h:mma')}`\n : `${leadingComma}${event.end.format('h:mma')} - ${event.start.format('h:mma')}`;\n}\n","import { Dayjs } from 'dayjs';\nimport { hasOverlap } from './hasOverlap';\nimport { CalendarEvent } from '~/types';\n\n/*\n Function that returns an array of events that overlap a week with given weekStartDate\n*/\nexport function filterByWeek<T extends CalendarEvent>(array: T[], weekStartDate: Dayjs) {\n const weekEndDate = weekStartDate.add(6, 'd');\n return array.filter(({ start, end }) => {\n const eventEnd = end || start; // Take event start if end is null\n return hasOverlap(weekStartDate, weekEndDate, start, eventEnd);\n });\n}\n","import { Dayjs } from 'dayjs';\nimport { isBetween } from './isBetween';\nimport { CalendarEvent } from '~/types';\n\n/**\n * Filters a list of calendar events to include only those that occur on a given date.\n *\n * An event is included if the `date` is:\n * - strictly between the event's `start` and `end` dates,\n * - equal to the `start` date,\n * - or equal to the `end` date.\n *\n * @param data - The list of calendar events to filter.\n * @param date - The target date to match against event ranges.\n * @returns An array of events that occur on the specified date.\n */\nexport function filterByDate<T extends CalendarEvent>(data: T[], date: Dayjs): T[] {\n return data.filter(({ start, end }) => isBetween(date, start, end));\n}\n","import { Dayjs } from 'dayjs';\nimport { hasOverlap } from './hasOverlap';\nimport { CalendarEvent, CalendarView } from '~/types';\n\n/**\n * Filters events that overlap with the visible range of a calendar view (month or year).\n *\n * @template T - Optional custom data attached to the calendar event.\n * @param {CalendarEvent<T>[]} events - List of calendar events.\n * @param {Dayjs} activeDate - The current date used to determine the visible time range.\n * @param {CalendarView} view - The current calendar view ('month' or 'year').\n * @returns {CalendarEvent<T>[]} An array of events that overlap the given view range.\n */\nexport function filterByView<T>(\n events: CalendarEvent<T>[],\n activeDate: Dayjs,\n view: CalendarView\n): CalendarEvent<T>[] {\n const unit = view === 'year' ? 'year' : 'month';\n\n const filterStart = activeDate.startOf(unit).day(0); // Align to Sunday\n const filterEnd = activeDate.endOf(unit).day(6); // Align to Saturday\n\n return events.filter(({ start, end }) => hasOverlap(filterStart, filterEnd, start, end));\n}\n","import dayjs from 'dayjs';\nimport { CalendarEvent, RawCalendarEvent } from '~/types';\n\nconst getDayjs = (rawDate: dayjs.Dayjs, timeString: string) => {\n const formatted = timeString.replace(/.{2}$/, ' $&').toUpperCase();\n return dayjs(`${rawDate.format('DD-MMM-YYYY')} ${formatted}`);\n};\n\nexport function parseRawEvents<T>(events: RawCalendarEvent<T>[]): CalendarEvent<T>[] {\n return events\n .filter((event) => event.start || event.end)\n .map((event, index) => {\n // Event start\n let dayjsStart = event.start ? dayjs(event.start) : dayjs(event.end);\n if (event.startTime) {\n dayjsStart = getDayjs(dayjsStart, event.startTime);\n }\n\n // Event end (default to event start)\n let dayjsEnd = event.end ? dayjs(event.end) : dayjsStart;\n\n // Add end time\n if (event.startTime || event.endTime || event.isAllDay === false) {\n if (!event.startTime && !event.endTime) {\n dayjsEnd = dayjsStart.add(1, 'hour'); // Neither start nor end time\n } else if (event.endTime) {\n dayjsEnd = getDayjs(dayjsEnd, event.endTime); // Only end Time\n } else {\n dayjsEnd = getDayjs(dayjsEnd, event.startTime!).add(1, 'hour'); // Only start time\n }\n }\n\n const parsedEvent = {\n ...event,\n id: event.id ?? index,\n title: event.title ?? '',\n start: dayjsStart,\n end: dayjsEnd,\n isAllDay: event.isAllDay !== false,\n };\n\n return parsedEvent;\n });\n}\n","import { Dayjs } from 'dayjs';\nimport { OrderedCalendarEvent } from '~/types';\n\n/*\n Returns the correct number of events for a cell without duplicates in a given week\n*/\nexport function getVisibleEvents<T extends OrderedCalendarEvent>(\n data: T[],\n date: Dayjs,\n maxEvents: number,\n isInDayHeader: boolean\n) {\n return data.filter(\n ({ order, start }) =>\n order < maxEvents && (isInDayHeader || date.day() === 0 || date.isSame(start, 'day'))\n );\n}\n","import { hasOverlap } from './hasOverlap';\nimport { CalendarEvent, OrderedCalendarEvent } from '~/types';\n\n/**\n * Sorts an array of calendar events with the following priority:\n * 1. Longer events appear first (descending by length in days).\n * 2. If lengths are equal, earlier start dates come first.\n * 3. If start dates are equal, all-day events come before timed events.\n *\n * @template T\n * @param {T[]} weekEventsArray - Array of calendar events to sort.\n * @returns {T[]} A new array sorted according to the above criteria.\n */\nfunction calendarSort<T extends CalendarEvent>(weekEventsArray: T[]) {\n const clone = [...weekEventsArray];\n\n return clone.sort((a, b) => {\n // Calculate the length of each event in days, including both start and end days\n const a_lengthInDays = a.end.diff(a.start, 'day') + 1;\n const b_lengthInDays = b.end.diff(b.start, 'day') + 1;\n\n // Sort order priority:\n // 1. Longer events come first (descending by length)\n if (a_lengthInDays > b_lengthInDays) return -1;\n if (a_lengthInDays < b_lengthInDays) return 1;\n\n // 2. If lengths are equal, earlier start date comes first\n if (a.start.isBefore(b.start, 'day')) return -1;\n if (b.start.isBefore(a.start, 'day')) return 1;\n\n // 3. If same start date, place all-day events above timed events\n if (a.isAllDay && !b.isAllDay) return -1;\n if (!a.isAllDay && b.isAllDay) return 1;\n\n // 4. If all criteria are equal, keep original order (stable sort)\n return 0;\n });\n}\n\n/**\n * Finds the lowest non-negative integer slot not present in the given set of occupied slots.\n *\n * @param {Set<number>} overlappingSlots - A set of slot numbers that are already taken.\n * @returns {number} The lowest slot number not in the set, starting from 0.\n */\nfunction findAvailableSlot(overlappingSlots: Set<number>): number {\n let slot = 0;\n while (overlappingSlots.has(slot)) slot++;\n return slot;\n}\n\n/*\n Function that takes an array of events for a single week and arranges them to create a dense fit\n*/\nexport function arrangeWeekEvents<T extends CalendarEvent>(\n weekEventsArray: T[]\n): (OrderedCalendarEvent & T)[] {\n if (weekEventsArray.length < 2)\n return weekEventsArray.map((event) => ({ ...{ ...event, order: 0, indent: 0 } }));\n\n // Sort events by length, then start date, then duration\n const sortedWeekEvents = calendarSort(weekEventsArray);\n\n // Extract first event from array and place as in first (top) slot/position\n const [firstEvent, ...followingEvents] = sortedWeekEvents;\n const orderedArray: OrderedCalendarEvent<T>[] = [{ ...firstEvent, order: 0, indent: 0 }];\n\n // Now loop through all other events and add them to the ordered array\n followingEvents.forEach((testEvent) => {\n const testStart = testEvent.start;\n const testEnd = testEvent.end || testStart; // fallback if no end\n\n // Assign 'slots' for each day cell. Track slots that contain overlapping entries\n const overlappingSlots = new Set<number>();\n\n // Check for overlaps with already placed events\n for (const placedEvent of orderedArray) {\n const placedStart = placedEvent.start;\n const placedEnd = placedEvent.end || placedStart; // fallback if no end\n\n if (hasOverlap(testStart, testEnd, placedStart, placedEnd)) {\n overlappingSlots.add(placedEvent.order);\n }\n }\n\n // Find the lowest slot number not taken by overlapping events\n const eventOrder = findAvailableSlot(overlappingSlots);\n\n // Add this event to the ordered array, along with its allocated slot\n orderedArray.push({ ...testEvent, order: eventOrder, indent: 0 });\n });\n return orderedArray;\n}\n","import { Dayjs } from 'dayjs';\n\nexport function returnValidStartEnd(start: Dayjs, end: Dayjs) {\n return !end\n ? [start, start]\n : start.isSame(end) || start.isBefore(end)\n ? [start, end]\n : [end, start];\n}\n","import dayjs from 'dayjs';\nimport { OrderedCalendarEvent } from '../types';\n\nexport const EMPTY_EVENT: OrderedCalendarEvent = {\n id: null,\n dragId: null,\n title: 'Untitled',\n groups: [],\n start: dayjs(),\n end: dayjs(),\n startTime: undefined,\n endTime: undefined,\n isActive: false,\n isAllDay: true,\n indent: 0,\n order: 0,\n};\n","import { Dayjs } from 'dayjs';\nimport { returnValidStartEnd } from './returnValidStartEnd';\nimport { EMPTY_EVENT } from '~/state/EMPTY_EVENT';\nimport { OrderedCalendarEvent } from '~/types';\n\nexport const getPlaceholderEvent = (\n start: Dayjs,\n end: Dayjs,\n isTimeEntry?: boolean,\n isInitialisation?: boolean\n) => {\n const [validStart, validEnd] = returnValidStartEnd(start, end);\n const eventEnd = isInitialisation && isTimeEntry ? validStart.add(15, 'minute') : validEnd;\n\n const updatedEvent: OrderedCalendarEvent = {\n ...EMPTY_EVENT,\n id: null,\n start: validStart,\n end: eventEnd,\n isActive: true,\n isAllDay: isTimeEntry ? false : true,\n startTime: isTimeEntry ? validStart.format('h:mma') : undefined,\n endTime: isTimeEntry ? eventEnd.format('h:mma') : undefined,\n order: isTimeEntry ? 100 : 0,\n };\n return updatedEvent;\n};\n","import { Dayjs } from 'dayjs';\nimport { getTimeDiff } from './getTimeDiff';\nimport { setTime } from './setTime';\nimport { CalendarEvent, OrderedCalendarEvent } from '~/types';\n\n/*\n Function that sorts events by start time, from earliest to latest.\n Only sort arrays with at least 2 events.\n*/\nfunction sortByStart<T extends CalendarEvent>(array: T[]) {\n if (array.length < 2) return array;\n\n return array.sort((a, b) => {\n const timeDiff = getTimeDiff(a.start, b.start);\n return timeDiff > 0 ? -1 : timeDiff < 0 ? 1 : 0;\n });\n}\n\n/*\n Function that checks if the start of a test event overlaps with another event\n*/\nfunction overlapsStart(test_start: Dayjs, main_start: Dayjs, main_end: Dayjs) {\n return (\n test_start.isSame(main_start) ||\n (test_start.isAfter(main_start) && test_start.isBefore(main_end))\n );\n}\n\n/*\n Function that takes an array of events for a single day and orders & indents them based on overlaps\n*/\nexport function arrangeWeekdayEvents<T extends CalendarEvent>(\n dayEvents: T[],\n date: Dayjs\n): (OrderedCalendarEvent & T)[] {\n if (dayEvents.length < 2) return dayEvents.map((event) => ({ ...event, order: 0, indent: 0 }));\n\n // Sort events by start time\n const sortedEvents = sortByStart(dayEvents);\n\n // if (date.day() === 4) {\n // \tconsole.log(dayEvents);\n // }\n\n // Extract first event from array and place it first with no indent\n const [firstEvent, ...followingEvents] = sortedEvents;\n const orderedArray: OrderedCalendarEvent<T>[] = [{ ...firstEvent, order: 0, indent: 0 }];\n\n // Now loop through all other events and add them to the ordered array\n followingEvents.forEach((testEvent, index) => {\n // Set the time for the next event to the correct day\n const tempStart_test_event = setTime(date, testEvent.start);\n\n // We loop through the current events already added to the ordered array and compare with the test event\n // If the start of the test event overlaps with another event, we increment the indent by 1\n let eventIndent = 0;\n orderedArray.forEach((currEvent) => {\n // Set the time for the comparison event to the correct day\n const tempStart_curr_event = setTime(date, currEvent.start);\n const tempEnd_curr_event = setTime(date, currEvent.end);\n\n if (overlapsStart(tempStart_test_event, tempStart_curr_event, tempEnd_curr_event)) {\n eventIndent += 1;\n }\n });\n\n // Add this event to the ordered array, noting its order and indent\n // (the array is already sorted so we use the index for ordering)\n orderedArray.push({ ...testEvent, order: index + 1, indent: eventIndent });\n });\n\n return orderedArray;\n}\n","import dayjs from 'dayjs';\n\nexport const createDayjsObjFromTime = (\n startTime: string | null,\n endTime: string | null,\n startString: string = '01-Jan-2000',\n endString: string = '01-Jan-2000'\n) => {\n const formattedStartTime = startTime?.replace(/.{2}$/, ' $&').toUpperCase() || '12:00 AM';\n const formattedEndTime = endTime?.replace(/.{2}$/, ' $&').toUpperCase() || '12:00 AM';\n const start = dayjs(`${startString} ${formattedStartTime}`);\n const end = dayjs(`${endString} ${formattedEndTime}`);\n return { start, end };\n};\n","import React from 'react';\nimport { CalendarGroup } from '~/types';\nimport { DEFAULT_COLOR } from '~/utils';\n\nexport function getBackgroundFromGroups(groups?: CalendarGroup[]): React.CSSProperties {\n const colors = groups?.map((g) => g.color).filter(Boolean) ?? [];\n\n if (!colors.length) return { backgroundColor: DEFAULT_COLOR };\n\n if (colors.length === 1) return { backgroundColor: colors[0] };\n\n const increment = 100 / colors.length;\n const values = colors\n .map((color, i) => `${color} ${increment * i}% ${increment * i + increment}%`)\n .join(', ');\n return { backgroundImage: `-webkit-linear-gradient(-30deg,${values})` };\n}\n","import { OrderedCalendarEvent } from '~/types';\nimport { getBackgroundFromGroups } from '~/utils';\n\nexport const getWeekOrDayEventStyles = (\n event: OrderedCalendarEvent,\n timeDuration: number,\n dayIndex: number,\n overlapOffset: number,\n isActive: boolean\n) => {\n const timeColumnOffset = dayIndex === 0 ? 6 : 0;\n\n return {\n gridColumnStart: dayIndex + 1,\n gridRowStart: event.start && event.start.hour() * 4 + Math.round(event.start.minute() / 15) + 1,\n gridRowEnd: event.end && event.end.hour() * 4 + Math.round(event.end.minute() / 15) + 1,\n height: 12 * ((timeDuration || 60) / 15) - 2,\n borderWidth: event.indent > 0 ? (timeDuration > 30 ? '1px' : '0.5px') : 0,\n marginLeft: overlapOffset * event.indent + timeColumnOffset,\n width: `calc(100% - ${overlapOffset * (event.indent + 1) + timeColumnOffset}px)`,\n zIndex: isActive ? 100 : 1 + event.order,\n ...getBackgroundFromGroups(event.groups),\n };\n};\n","import dayjs from 'dayjs';\n\nimport './AllDayEvent.css';\n\nimport { getClipPath, getOverflowArrows } from './utils';\nimport { CalendarEvent, MinMaxDatesInView } from '~/types';\nimport { getBackgroundFromGroups } from '~/utils';\n\ninterface AllDayEventProps {\n date: dayjs.Dayjs;\n event: CalendarEvent;\n minMaxDatesInView?: MinMaxDatesInView;\n isCompact: boolean;\n isInOverflow: boolean;\n}\n\nexport function AllDayEvent({\n date,\n event,\n minMaxDatesInView,\n isCompact,\n isInOverflow,\n}: AllDayEventProps) {\n // Calculate arrows for display in overflow popover\n const overflowArrows = getOverflowArrows(\n isInOverflow,\n date,\n event.start,\n event.end,\n minMaxDatesInView\n );\n\n // Event background color(s)\n const clipPath = getClipPath(overflowArrows);\n const colorstyles = getBackgroundFromGroups(event.groups);\n\n return (\n <div className=\"events-calendar-all-day-event\" style={{ ...colorstyles, clipPath }}>\n <div className=\"events-calendar-all-day-event-content\" data-sm={isCompact}>\n <span className=\"events-calendar-all-day-event-text\" data-arrows={overflowArrows}>\n {event.title}\n </span>\n </div>\n </div>\n );\n}\n","import './TimeEvent.css';\n\nimport { CalendarEvent } from '~/types';\nimport { DEFAULT_COLOR, getBackgroundFromGroups } from '~/utils';\n\ninterface TimeEventProps {\n event: CalendarEvent;\n isCompact: boolean;\n}\n\nexport function TimeEvent({ event, isCompact }: TimeEventProps) {\n // Destructure event\n const { title, start, groups } = event;\n\n // Event background color(s)\n const colorStyles = getBackgroundFromGroups(groups);\n const eventColors = groups?.map((g) => g.color).filter(Boolean) ?? [];\n const borderStyle = { border: `1px solid ${eventColors[0] ?? DEFAULT_COLOR}` };\n\n return (\n <div className=\"events-calendar-time\" data-sm={isCompact} style={borderStyle}>\n <div className=\"events-calendar-time-dot\" style={colorStyles}></div>\n <div className=\"events-calendar-time-text\">{start.format('h:mma')}</div>\n <span className=\"events-calendar-time-label\">{title}</span>\n </div>\n );\n}\n","import { Dayjs } from 'dayjs';\nimport { Dispatch } from 'react';\nimport { getPlaceholderEvent, getTimeDiff, returnValidStartEnd } from '../utils';\nimport { CalendarAction, CalendarState, EventEditProps, MouseEventHandler } from '~/types';\n\ninterface useMouseEventProps {\n enableDragCreation: boolean;\n dispatch: Dispatch<CalendarAction>;\n state: CalendarState;\n onEventCreate?: (props: EventEditProps) => void;\n}\n\nconst updatePlaceholder = ({\n isTimeEvent,\n firstClickDate,\n start,\n date,\n dispatch,\n}: {\n isTimeEvent: boolean;\n firstClickDate: Dayjs;\n start: Dayjs;\n date: Dayjs;\n dispatch: Dispatch<CalendarAction>;\n}) => {\n if (isTimeEvent) {\n const tempStart =\n getTimeDiff(firstClickDate, date) < 0\n ? returnValidStartEnd(start, firstClickDate.add(15, 'minute'))[1]\n : firstClickDate;\n const tempEnd = start.hour(date.hour()).minute(date.minute());\n if (Math.abs(getTimeDiff(tempStart, date)) >= 15) {\n const updatedEvent = getPlaceholderEvent(tempStart, tempEnd, true);\n dispatch({ type: 'update_event', event: updatedEvent });\n }\n } else {\n const updatedEvent = getPlaceholderEvent(firstClickDate, date);\n dispatch({ type: 'update_event', event: updatedEvent });\n }\n};\n\nexport const useMouseEvent = ({\n enableDragCreation,\n dispatch,\n state,\n onEventCreate,\n}: useMouseEventProps) => {\n // If view only calendar, only close popovers on mousedown\n if (!enableDragCreation) {\n return (e: React.MouseEvent) => {\n if (e.type === 'mousedown') dispatch({ type: 'reset_calendar' });\n };\n }\n\n // Popover handler\n const openPopover = () => dispatch({ type: 'open_popover' });\n const reset = () => dispatch({ type: 'reset_calendar' });\n\n const mouseEventHandler: MouseEventHandler = (e, date, isTimeEvent, placeholderRef) => {\n const { dragActive, firstClickDate, placeholderEvent } = state;\n const { start } = placeholderEvent;\n\n // Reset drag event if anything other than left click triggered\n if (e.button !== 0) {\n if (dragActive) dispatch({ type: 'event_create_stop' });\n return;\n }\n\n switch (e.type) {\n case 'mousedown': {\n if (state.eventAnchor || state.overflowIsOpen) {\n dispatch({ type: 'reset_calendar' });\n return;\n }\n\n const updatedEvent = getPlaceholderEvent(date, date, isTimeEvent, true);\n dispatch({\n type: 'event_create_start',\n date,\n event: updatedEvent,\n anchor: placeholderRef.current,\n });\n break;\n }\n case 'mouseenter': {\n if (e.buttons !== 1 || !dragActive || !firstClickDate) return;\n updatePlaceholder({ isTimeEvent, firstClickDate, start, date, dispatch });\n break;\n }\n case 'mouseup': {\n if (!dragActive) return;\n\n onEventCreate &&\n onEventCreate({\n clickedEvent: state.clickedEvent,\n newEvent: state.placeholderEvent,\n eventRef: placeholderRef.current!,\n openPopover,\n reset,\n });\n\n dispatch({ type: 'event_create_end', anchor: placeholderRef.current });\n }\n }\n };\n return mouseEventHandler;\n};\n","/* eslint-disable react-hooks/exhaustive-deps */\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\ntype ObserverRect = Omit<DOMRectReadOnly, 'toJSON'>;\n\nconst defaultState: ObserverRect = {\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n left: 0,\n bottom: 0,\n right: 0,\n};\n\nexport function useResizeObserver<T extends HTMLDivElement = HTMLDivElement>(\n options?: ResizeObserverOptions\n) {\n const frameID = useRef(0);\n const ref = useRef<T>(null);\n\n const [rect, setRect] = useState<ObserverRect>(defaultState);\n\n const observer = useMemo(\n () =>\n typeof window !== 'undefined'\n ? new ResizeObserver((entries: ResizeObserverEntry[]) => {\n const entry = entries[0];\n\n if (entry) {\n cancelAnimationFrame(frameID.current);\n\n frameID.current = requestAnimationFrame(() => {\n if (ref.current) {\n setRect(entry.contentRect);\n }\n });\n }\n })\n : null,\n []\n );\n\n useEffect(() => {\n if (ref.current) {\n observer?.observe(ref.current, options);\n }\n\n return () => {\n observer?.disconnect();\n\n if (frameID.current) {\n cancelAnimationFrame(frameID.current);\n }\n };\n }, [ref.current]);\n\n return [ref!, rect] as const;\n}\n\nexport function useElementSize<T extends HTMLDivElement = HTMLDivElement>(\n options?: ResizeObserverOptions\n) {\n const [ref, { width, height }] = useResizeObserver<T>(options);\n return { ref, width, height };\n}\n","'use client';\n\nimport { Dispatch, MouseEvent, RefObject, useState } from 'react';\nimport { flip, offset, shift, useDismiss, useFloating, useInteractions } from '@floating-ui/react';\nimport { CalendarAction, CalendarEvent, CalendarState } from '~/types';\n\ninterface useCalendarEventProps<T> {\n dispatch: Dispatch<CalendarAction>;\n event: CalendarEvent<T>;\n isInOverflow: boolean;\n hasContextMenu: boolean;\n state: CalendarState;\n}\n\nexport function useCalendarEvent<T>({\n dispatch,\n event,\n isInOverflow,\n hasContextMenu,\n state,\n}: useCalendarEventProps<T>) {\n // Context menu state\n const [contextIsOpen, setContextIsOpen] = useState(false);\n const closeContextMenu = () => setContextIsOpen(false);\n\n // Constants\n const isActive = event.id !== null && state.clickedEvent?.id === event.id;\n\n const { refs, floatingStyles, context } = useFloating({\n open: contextIsOpen,\n onOpenChange: setContextIsOpen,\n middleware: [\n offset({ mainAxis: 5, alignmentAxis: 4 }),\n flip({\n fallbackPlacements: ['left-start'],\n }),\n shift({ padding: 10 }),\n ],\n placement: 'right-start',\n // strategy: 'fixed',\n // whileElementsMounted: autoUpdate,\n });\n\n const dismiss = useDismiss(context);\n\n const { getFloatingProps } = useInteractions([dismiss]);\n\n // Handle event right click (context menu)\n const handleContextMenu = (e: MouseEvent, eventRef: RefObject<HTMLDivElement | null>) => {\n if (!hasContextMenu) return;\n e.preventDefault();\n\n if (state.popoverIsOpen || (state.overflowIsOpen && !isInOverflow)) return;\n\n refs.setPositionReference({\n getBoundingClientRect() {\n return {\n width: 0,\n height: 0,\n x: e.clientX,\n y: e.clientY,\n top: e.clientY,\n right: e.clientX,\n bottom: e.clientY,\n left: e.clientX,\n };\n },\n });\n\n dispatch({ type: 'open_context_menu', event, anchor: eventRef.current });\n setContextIsOpen(true);\n };\n\n return {\n handleContextMenu,\n isActive,\n contextIsOpen,\n refs,\n floatingStyles,\n getFloatingProps,\n closeContextMenu,\n };\n}\n","'use client';\n\nimport dayjs, { Dayjs } from 'dayjs';\nimport { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react';\nimport { CalendarAction, CalendarState, CalendarView } from '../types';\nimport { DEFAULT_STATE, reducer } from '~/state';\n\nexport type EventsCalendarObject = {\n activeDate: Dayjs;\n setActiveDate: Dispatch<SetStateAction<Dayjs>>;\n view: CalendarView;\n setView: Dispatch<SetStateAction<CalendarView>>;\n state: CalendarState;\n dispatch: Dispatch<CalendarAction>;\n};\n\nexport type useEventsCalendarProps = {\n isInitialised?: boolean;\n initialDate?: string | number | Date | Dayjs;\n initialView?: CalendarView;\n closeOnClickOutside?: boolean;\n};\n\nexport function useEventsCalendar({\n isInitialised = false,\n initialDate = dayjs(),\n initialView = 'month',\n closeOnClickOutside = true,\n}: Partial<useEventsCalendarProps> = {}): EventsCalendarObject {\n const [activeDate, setActiveDate] = useState(dayjs(initialDate));\n const [view, setView] = useState<CalendarView>(initialView);\n\n const [state, dispatch] = useReducer(reducer, DEFAULT_STATE);\n\n // Reset calendar on click outside of the react component\n useEffect(() => {\n if (!closeOnClickOutside || isInitialised) return;\n const handleClose = () => dispatch({ type: 'reset_calendar' });\n\n window.addEventListener('click', handleClose);\n return () => {\n window.removeEventListener('click', handleClose);\n };\n }, [dispatch, isInitialised, closeOnClickOutside]);\n\n return {\n activeDate,\n setActiveDate,\n view,\n setView,\n state,\n dispatch,\n };\n}\n","import { CalendarState } from '../types';\nimport { EMPTY_EVENT } from './EMPTY_EVENT';\n\nexport const DEFAULT_STATE: CalendarState = {\n // View/edit event\n clickedEvent: EMPTY_EVENT,\n popoverIsOpen: false,\n eventAnchor: null,\n\n // Drag creation\n dragActive: false,\n firstClickDate: null,\n placeholderEvent: EMPTY_EVENT,\n\n // Event Drag & drop\n dragStartOffset: null,\n eventDragActive: false,\n\n // Overflow Popover\n overflowAnchor: null,\n overflowIsOpen: false,\n};\n","import { DEFAULT_STATE } from './DEFAULT_STATE';\nimport { EMPTY_EVENT } from './EMPTY_EVENT';\nimport { CalendarAction, CalendarState } from '~/types';\n\nexport function reducer(state: CalendarState