1- import { FC , useRef , useState , useEffect , useCallback } from "react" ;
2- import { ViewContentArg , DatesSetArg , EventMountArg } from "@fullcalendar/core" ;
1+ import { FC , useRef , useEffect , useCallback } from "react" ;
2+ import { ViewContentArg , DatesSetArg } from "@fullcalendar/core" ;
33import dayGridPlugin from "@fullcalendar/daygrid" ;
44import interactionPlugin , { DateClickArg } from "@fullcalendar/interaction" ;
55import FullCalendar from "@fullcalendar/react" ;
@@ -56,59 +56,96 @@ export const getCoords = (elem: HTMLElement | null): TElemCoords => {
5656} ;
5757
5858const MAX_DOTS_PER_DAY = 30 ;
59+ const DOT_CLASS =
60+ "fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-today" ;
5961
60- // Track which events have been added to each day to avoid race conditions
61- const dayEventCounts = new Map < string , Set < string > > ( ) ;
62-
63- const eventRender = ( info : EventMountArg , widgetHash : string ) => {
64- const { event, backgroundColor } = info ;
62+ /**
63+ * Build all event dots in a single batch pass instead of per-event callbacks.
64+ * Uses a pre-built day cell map to avoid repeated DOM queries,
65+ * native Date arithmetic instead of moment, and DocumentFragment
66+ * for a single DOM write per day cell.
67+ */
68+ const renderEventDots = ( events : TEvent [ ] , widgetHash : string ) : void => {
6569 const cal = document . querySelector ( `#cal-${ widgetHash } ` ) ;
66- const { start } = event ;
67- const end = moment ( event . end || start )
68- . add ( 1 , "days" )
69- . format ( ) ;
70-
71- const now = moment ( start ) ;
72- if ( cal ) {
73- while ( now . isBefore ( end , "day" ) ) {
74- const dateStr = now . format ( "YYYY-MM-DD" ) ;
75- const daygrid = cal . querySelector (
76- `.fc-daygrid-day[data-date="${ dateStr } "] .fc-daygrid-day-frame .fc-daygrid-day-events`
77- ) ;
78-
79- if ( daygrid ) {
80- // Initialize tracking for this day if needed
81- if ( ! dayEventCounts . has ( dateStr ) ) {
82- dayEventCounts . set ( dateStr , new Set ( ) ) ;
83- }
84-
85- const eventsForDay = dayEventCounts . get ( dateStr ) ;
86- if ( eventsForDay ) {
87- // Check if we haven't already added this event and haven't exceeded limit
88- if (
89- ! eventsForDay . has ( event . id ) &&
90- eventsForDay . size < MAX_DOTS_PER_DAY
91- ) {
92- const prevDot = daygrid . querySelector (
93- `[data-id="${ event . id } "]`
94- ) ;
95- if ( ! prevDot ) {
96- const dot = document . createElement ( "span" ) ;
97- dot . style . backgroundColor = backgroundColor ;
98- dot . className =
99- "fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-today" ;
100- dot . setAttribute ( "data-id" , event . id ) ;
101- daygrid . append ( dot ) ;
102-
103- // Track that we added this event
104- eventsForDay . add ( event . id ) ;
105- }
70+ if ( ! cal ) return ;
71+
72+ // Clear any previously rendered dots and "+X more" links before re-rendering
73+ cal . querySelectorAll (
74+ `.${ DOT_CLASS . split ( " " ) [ 0 ] } , .fc-daygrid-more-link`
75+ ) . forEach ( ( el ) => el . remove ( ) ) ;
76+
77+ // Build day cell lookup map once (max 42 cells in a month grid)
78+ const dayCellMap = new Map < string , Element > ( ) ;
79+ cal . querySelectorAll ( ".fc-daygrid-day" ) . forEach ( ( cell ) => {
80+ const date = cell . getAttribute ( "data-date" ) ;
81+ const container = cell . querySelector (
82+ ".fc-daygrid-day-frame .fc-daygrid-day-events"
83+ ) ;
84+ if ( date && container ) dayCellMap . set ( date , container ) ;
85+ } ) ;
86+
87+ // Group dots by day
88+ const dotsByDay = new Map <
89+ string ,
90+ { id : string ; color : string | undefined } [ ]
91+ > ( ) ;
92+
93+ events
94+ . filter ( ( event ) => event . start )
95+ . forEach ( ( event ) => {
96+ const startTime = new Date ( event . start ) ;
97+ const endTime =
98+ new Date ( event . end || event . start ) . getTime ( ) + 86_400_000 ;
99+ const current = new Date ( startTime ) ;
100+
101+ while ( current . getTime ( ) < endTime ) {
102+ const dateStr = `${ current . getFullYear ( ) } -${ String ( current . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) } -${ String ( current . getDate ( ) ) . padStart ( 2 , "0" ) } ` ;
103+ if ( dayCellMap . has ( dateStr ) ) {
104+ let dayDots = dotsByDay . get ( dateStr ) ;
105+ if ( ! dayDots ) {
106+ dayDots = [ ] ;
107+ dotsByDay . set ( dateStr , dayDots ) ;
106108 }
109+ dayDots . push ( {
110+ id : event . id ,
111+ color : event . backgroundColor ,
112+ } ) ;
107113 }
114+ current . setDate ( current . getDate ( ) + 1 ) ;
115+ }
116+ } ) ;
117+
118+ // Render dots using DocumentFragment (single DOM write per day)
119+ Array . from ( dotsByDay . entries ( ) ) . forEach ( ( [ dateStr , dots ] ) => {
120+ const container = dayCellMap . get ( dateStr ) ;
121+ if ( ! container ) return ;
122+
123+ const fragment = document . createDocumentFragment ( ) ;
124+ const seen = new Set < string > ( ) ;
125+ let overflow = 0 ;
126+
127+ dots . forEach ( ( dot ) => {
128+ if ( seen . has ( dot . id ) ) return ;
129+ seen . add ( dot . id ) ;
130+ if ( seen . size <= MAX_DOTS_PER_DAY ) {
131+ const span = document . createElement ( "span" ) ;
132+ if ( dot . color ) span . style . backgroundColor = dot . color ;
133+ span . className = DOT_CLASS ;
134+ fragment . appendChild ( span ) ;
135+ } else {
136+ overflow += 1 ;
108137 }
109- now . add ( 1 , "days" ) ;
138+ } ) ;
139+
140+ if ( overflow > 0 ) {
141+ const moreEl = document . createElement ( "span" ) ;
142+ moreEl . className = "fc-daygrid-more-link fc-more-link text-[11px]" ;
143+ moreEl . textContent = `+${ overflow } more` ;
144+ fragment . appendChild ( moreEl ) ;
110145 }
111- }
146+
147+ container . appendChild ( fragment ) ;
148+ } ) ;
112149} ;
113150
114151interface ICalendarMonth extends ICalendarBaseProps {
@@ -140,7 +177,7 @@ export const CalendarMonth: FC<ICalendarMonth> = ({
140177 events,
141178 onDatesSet,
142179 showFullSize,
143- catFilters,
180+ catFilters : _catFilters ,
144181 selectedDate,
145182 widgetHash,
146183 handleHeaderTooltips,
@@ -149,9 +186,9 @@ export const CalendarMonth: FC<ICalendarMonth> = ({
149186 isAlphaModalOpen,
150187 handleIsAlphaModalOpen,
151188} ) => {
152- const [ key , setKey ] = useState ( 0 ) ; // to force calendar to rerender
153-
154189 const calendarRef = useRef < FullCalendar > ( null ) ;
190+ const eventsRef = useRef ( events ) ;
191+ eventsRef . current = events ;
155192
156193 const handleSize = ( event : ViewContentArg ) : void => {
157194 const contentAPi = event . view . calendar ;
@@ -296,6 +333,10 @@ export const CalendarMonth: FC<ICalendarMonth> = ({
296333 const handleNewMonthView = useCallback (
297334 ( info : DatesSetArg ) => {
298335 handleHeaderTooltips ( info , widgetHash , showFullSize ) ;
336+ // Re-render dots after FC has swapped grid cells for the new month
337+ if ( eventsRef . current ?. length ) {
338+ renderEventDots ( eventsRef . current , widgetHash ) ;
339+ }
299340 if ( onDatesSet != null ) {
300341 onDatesSet ( info . view . currentStart . toString ( ) ) ;
301342 }
@@ -360,22 +401,31 @@ export const CalendarMonth: FC<ICalendarMonth> = ({
360401 n2 ?. setAttribute ( "data-tip" , "Next Week" ) ;
361402 n2 ?. setAttribute ( "data-place" , "right" ) ;
362403 }
404+
405+ // Render dots on initial mount when events are already available
406+ if ( eventsRef . current ?. length ) {
407+ renderEventDots ( eventsRef . current , widgetHash ) ;
408+ }
363409 }
364410 } ,
365411 [ widgetHash ]
366412 ) ;
367413
414+ // Re-render dots when event data changes (e.g., new data from queries).
415+ // viewDidMount and datesSet handle lifecycle-driven rendering;
416+ // this effect handles data-driven updates when the grid is already stable.
368417 useEffect ( ( ) => {
369- // Clear the day event counts when calendar re-renders
370- dayEventCounts . clear ( ) ;
371- setKey ( ( prev ) => prev + 1 ) ;
372- } , [ events , catFilters ] ) ;
418+ if ( ! events ?. length ) return undefined ;
419+ const rafId = requestAnimationFrame ( ( ) => {
420+ renderEventDots ( events , widgetHash ) ;
421+ } ) ;
422+ return ( ) => cancelAnimationFrame ( rafId ) ;
423+ } , [ events , widgetHash ] ) ;
373424
374425 return (
375426 < FullCalendar
376427 initialDate = { selectedDate }
377428 locale = { locale }
378- key = { key }
379429 plugins = { [ dayGridPlugin , interactionPlugin ] }
380430 dayMaxEvents = { MAX_DOTS_PER_DAY }
381431 headerToolbar = { {
@@ -394,9 +444,8 @@ export const CalendarMonth: FC<ICalendarMonth> = ({
394444 } ,
395445 } }
396446 navLinkDayClick = { ( ) => { } } // this controls the date number click
397- eventDisplay = "block "
447+ eventDisplay = "none "
398448 events = { events }
399- eventDidMount = { ( ...args ) => eventRender ( ...args , widgetHash ) }
400449 ref = { calendarRef }
401450 windowResize = { handleSize }
402451 contentHeight = "auto"
0 commit comments