From 2b286bf90b40ae9b8bb143ea12534e225e9e1fff Mon Sep 17 00:00:00 2001 From: Xharles Date: Fri, 30 Jan 2026 13:17:28 +0100 Subject: [PATCH 1/7] feat: Optimize event dot rendering in CalendarMonth component --- .../src/components/calendar/CalendarMonth.tsx | 137 +++++++++++------- 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx index 399df00f9..5bc699bf6 100644 --- a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx +++ b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx @@ -1,5 +1,5 @@ import { FC, useRef, useState, useEffect, useCallback } from "react"; -import { ViewContentArg, DatesSetArg, EventMountArg } from "@fullcalendar/core"; +import { ViewContentArg, DatesSetArg } from "@fullcalendar/core"; import dayGridPlugin from "@fullcalendar/daygrid"; import interactionPlugin, { DateClickArg } from "@fullcalendar/interaction"; import FullCalendar from "@fullcalendar/react"; @@ -56,59 +56,83 @@ export const getCoords = (elem: HTMLElement | null): TElemCoords => { }; const MAX_DOTS_PER_DAY = 30; +const DOT_CLASS = + "fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-today"; -// Track which events have been added to each day to avoid race conditions -const dayEventCounts = new Map>(); - -const eventRender = (info: EventMountArg, widgetHash: string) => { - const { event, backgroundColor } = info; +/** + * Build all event dots in a single batch pass instead of per-event callbacks. + * Uses a pre-built day cell map to avoid repeated DOM queries, + * native Date arithmetic instead of moment, and DocumentFragment + * for a single DOM write per day cell. + */ +const renderEventDots = ( + events: TEvent[], + widgetHash: string +): void => { const cal = document.querySelector(`#cal-${widgetHash}`); - const { start } = event; - const end = moment(event.end || start) - .add(1, "days") - .format(); - - const now = moment(start); - if (cal) { - while (now.isBefore(end, "day")) { - const dateStr = now.format("YYYY-MM-DD"); - const daygrid = cal.querySelector( - `.fc-daygrid-day[data-date="${dateStr}"] .fc-daygrid-day-frame .fc-daygrid-day-events` - ); - - if (daygrid) { - // Initialize tracking for this day if needed - if (!dayEventCounts.has(dateStr)) { - dayEventCounts.set(dateStr, new Set()); - } - - const eventsForDay = dayEventCounts.get(dateStr); - if (eventsForDay) { - // Check if we haven't already added this event and haven't exceeded limit - if ( - !eventsForDay.has(event.id) && - eventsForDay.size < MAX_DOTS_PER_DAY - ) { - const prevDot = daygrid.querySelector( - `[data-id="${event.id}"]` - ); - if (!prevDot) { - const dot = document.createElement("span"); - dot.style.backgroundColor = backgroundColor; - dot.className = - "fc-daygrid-event fc-daygrid-block-event fc-h-event fc-event fc-event-start fc-event-end fc-event-today"; - dot.setAttribute("data-id", event.id); - daygrid.append(dot); - - // Track that we added this event - eventsForDay.add(event.id); - } - } + if (!cal) return; + + // Build day cell lookup map once (max 42 cells in a month grid) + const dayCellMap = new Map(); + cal.querySelectorAll(".fc-daygrid-day").forEach((cell) => { + const date = cell.getAttribute("data-date"); + const container = cell.querySelector( + ".fc-daygrid-day-frame .fc-daygrid-day-events" + ); + if (date && container) dayCellMap.set(date, container); + }); + + // Group dots by day + const dotsByDay = new Map< + string, + { id: string; color: string | undefined }[] + >(); + + for (const event of events) { + if (!event.start) continue; + const startTime = new Date(event.start); + const endTime = + new Date(event.end || event.start).getTime() + 86_400_000; + const current = new Date(startTime); + + while (current.getTime() < endTime) { + const dateStr = current.toISOString().slice(0, 10); + if (dayCellMap.has(dateStr)) { + let dayDots = dotsByDay.get(dateStr); + if (!dayDots) { + dayDots = []; + dotsByDay.set(dateStr, dayDots); } + dayDots.push({ + id: event.id, + color: event.backgroundColor, + }); } - now.add(1, "days"); + current.setDate(current.getDate() + 1); } } + + // Render dots using DocumentFragment (single DOM write per day) + for (const [dateStr, dots] of dotsByDay) { + const container = dayCellMap.get(dateStr); + if (!container) continue; + + const fragment = document.createDocumentFragment(); + const seen = new Set(); + let count = 0; + + for (const dot of dots) { + if (seen.has(dot.id) || count >= MAX_DOTS_PER_DAY) continue; + seen.add(dot.id); + const span = document.createElement("span"); + if (dot.color) span.style.backgroundColor = dot.color; + span.className = DOT_CLASS; + fragment.appendChild(span); + count++; + } + + container.appendChild(fragment); + } }; interface ICalendarMonth extends ICalendarBaseProps { @@ -365,12 +389,22 @@ export const CalendarMonth: FC = ({ [widgetHash] ); + // Force remount when events or filters change useEffect(() => { - // Clear the day event counts when calendar re-renders - dayEventCounts.clear(); setKey((prev) => prev + 1); }, [events, catFilters]); + // Batch-render dots after FullCalendar has mounted into the DOM. + // We use requestAnimationFrame to ensure the calendar grid cells + // exist before we query them. + useEffect(() => { + if (!events?.length) return; + const rafId = requestAnimationFrame(() => { + renderEventDots(events, widgetHash); + }); + return () => cancelAnimationFrame(rafId); + }, [key, events, widgetHash]); + return ( = ({ }, }} navLinkDayClick={() => {}} // this controls the date number click - eventDisplay="block" + eventDisplay="none" events={events} - eventDidMount={(...args) => eventRender(...args, widgetHash)} ref={calendarRef} windowResize={handleSize} contentHeight="auto" From 438fd9a3bca8fb6182912243dfac73db9251cec7 Mon Sep 17 00:00:00 2001 From: Xharles Date: Fri, 30 Jan 2026 13:17:57 +0100 Subject: [PATCH 2/7] feat: Increase QUERY_EVENTS_HARD_LIMIT to improve event query performance --- packages/frontend/src/config/widgets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/config/widgets.ts b/packages/frontend/src/config/widgets.ts index 22a82ab90..11720464b 100644 --- a/packages/frontend/src/config/widgets.ts +++ b/packages/frontend/src/config/widgets.ts @@ -14,7 +14,7 @@ export const WIDGETS_CONFIG = { }, [ETemplateNameRegistry.Calendar]: { TAG_ITEM_TYPE: "event", - QUERY_EVENTS_HARD_LIMIT: 200, + QUERY_EVENTS_HARD_LIMIT: 500, WIDGET_HEIGHT: 604, // gotten from cal-month POLLING_INTERVAL: 180 * 60, // 3h ADJUSTABLE: true, From e020d3f0e9e1e2f532c77c89771d1bfd0a03384a Mon Sep 17 00:00:00 2001 From: Xharles Date: Fri, 30 Jan 2026 13:26:38 +0100 Subject: [PATCH 3/7] refactor: Simplify renderEventDots function for improved readability and performance --- .../src/components/calendar/CalendarMonth.tsx | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx index 5bc699bf6..102083316 100644 --- a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx +++ b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx @@ -65,10 +65,7 @@ const DOT_CLASS = * native Date arithmetic instead of moment, and DocumentFragment * for a single DOM write per day cell. */ -const renderEventDots = ( - events: TEvent[], - widgetHash: string -): void => { +const renderEventDots = (events: TEvent[], widgetHash: string): void => { const cal = document.querySelector(`#cal-${widgetHash}`); if (!cal) return; @@ -88,51 +85,51 @@ const renderEventDots = ( { id: string; color: string | undefined }[] >(); - for (const event of events) { - if (!event.start) continue; - const startTime = new Date(event.start); - const endTime = - new Date(event.end || event.start).getTime() + 86_400_000; - const current = new Date(startTime); - - while (current.getTime() < endTime) { - const dateStr = current.toISOString().slice(0, 10); - if (dayCellMap.has(dateStr)) { - let dayDots = dotsByDay.get(dateStr); - if (!dayDots) { - dayDots = []; - dotsByDay.set(dateStr, dayDots); + events + .filter((event) => event.start) + .forEach((event) => { + const startTime = new Date(event.start); + const endTime = + new Date(event.end || event.start).getTime() + 86_400_000; + const current = new Date(startTime); + + while (current.getTime() < endTime) { + const dateStr = current.toISOString().slice(0, 10); + if (dayCellMap.has(dateStr)) { + let dayDots = dotsByDay.get(dateStr); + if (!dayDots) { + dayDots = []; + dotsByDay.set(dateStr, dayDots); + } + dayDots.push({ + id: event.id, + color: event.backgroundColor, + }); } - dayDots.push({ - id: event.id, - color: event.backgroundColor, - }); + current.setDate(current.getDate() + 1); } - current.setDate(current.getDate() + 1); - } - } + }); // Render dots using DocumentFragment (single DOM write per day) - for (const [dateStr, dots] of dotsByDay) { + Array.from(dotsByDay.entries()).forEach(([dateStr, dots]) => { const container = dayCellMap.get(dateStr); - if (!container) continue; + if (!container) return; const fragment = document.createDocumentFragment(); const seen = new Set(); - let count = 0; - for (const dot of dots) { - if (seen.has(dot.id) || count >= MAX_DOTS_PER_DAY) continue; + dots.filter( + (dot) => !seen.has(dot.id) && seen.size < MAX_DOTS_PER_DAY + ).forEach((dot) => { seen.add(dot.id); const span = document.createElement("span"); if (dot.color) span.style.backgroundColor = dot.color; span.className = DOT_CLASS; fragment.appendChild(span); - count++; - } + }); container.appendChild(fragment); - } + }); }; interface ICalendarMonth extends ICalendarBaseProps { @@ -398,7 +395,7 @@ export const CalendarMonth: FC = ({ // We use requestAnimationFrame to ensure the calendar grid cells // exist before we query them. useEffect(() => { - if (!events?.length) return; + if (!events?.length) return undefined; const rafId = requestAnimationFrame(() => { renderEventDots(events, widgetHash); }); From 5cb19b0415ae8a05de9d69a8cf99c321c19c9d32 Mon Sep 17 00:00:00 2001 From: Xharles Date: Fri, 30 Jan 2026 13:43:53 +0100 Subject: [PATCH 4/7] feat: Enhance calendar event fetching by merging events from previous, current, and next months --- .../containers/calendar/CalendarContainer.tsx | 76 ++++++++++++++++--- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/frontend/src/containers/calendar/CalendarContainer.tsx b/packages/frontend/src/containers/calendar/CalendarContainer.tsx index c95022970..cb2a4430f 100644 --- a/packages/frontend/src/containers/calendar/CalendarContainer.tsx +++ b/packages/frontend/src/containers/calendar/CalendarContainer.tsx @@ -174,29 +174,81 @@ const CalendarContainer: FC> = ({ const pollingInterval = (moduleData.widget.refresh_interval || POLLING_INTERVAL) * 1000; + const monthStart = moment(selectedDate).startOf("month"); + const tagsParam = tags ? filteringListToStr(tags) : undefined; + const queryOpts = { skip: !selectedDate, pollingInterval }; + const { - data: eventsData, - isLoading: isLoadingEvents, - isFetching: isFetchingEvents, + data: prevMonthData, + isLoading: isLoadingPrev, + isFetching: isFetchingPrev, } = useGetEventsQuery( { - period_after: moment(selectedDate) - .startOf("month") + period_after: monthStart + .clone() .subtract(1, "month") .format("YYYY-MM-DD"), - period_before: moment(selectedDate) - .startOf("month") + period_before: monthStart.format("YYYY-MM-DD"), + limit: QUERY_EVENTS_HARD_LIMIT, + tags: tagsParam, + }, + queryOpts + ); + + const { + data: currMonthData, + isLoading: isLoadingCurr, + isFetching: isFetchingCurr, + } = useGetEventsQuery( + { + period_after: monthStart.format("YYYY-MM-DD"), + period_before: monthStart + .clone() + .add(1, "month") + .format("YYYY-MM-DD"), + limit: QUERY_EVENTS_HARD_LIMIT, + tags: tagsParam, + }, + queryOpts + ); + + const { + data: nextMonthData, + isLoading: isLoadingNext, + isFetching: isFetchingNext, + } = useGetEventsQuery( + { + period_after: monthStart + .clone() .add(1, "month") .format("YYYY-MM-DD"), + period_before: monthStart + .clone() + .add(2, "months") + .format("YYYY-MM-DD"), limit: QUERY_EVENTS_HARD_LIMIT, - tags: tags ? filteringListToStr(tags) : undefined, + tags: tagsParam, }, - { skip: !selectedDate, pollingInterval } + queryOpts ); + const mergedEvents = useMemo(() => { + const prev = prevMonthData?.results ?? []; + const curr = currMonthData?.results ?? []; + const next = nextMonthData?.results ?? []; + return [...prev, ...curr, ...next]; + }, [ + prevMonthData?.results, + currMonthData?.results, + nextMonthData?.results, + ]); + + const isLoadingEvents = isLoadingPrev || isLoadingCurr || isLoadingNext; + const isFetchingEvents = isFetchingPrev || isFetchingCurr || isFetchingNext; + const closestEvent: TEvent | undefined = useMemo( - () => getClosestEvent(eventsData?.results, selectedDate), - [eventsData?.results, selectedDate] + () => getClosestEvent(mergedEvents, selectedDate), + [mergedEvents, selectedDate] ); const { @@ -305,7 +357,7 @@ const CalendarContainer: FC> = ({ return ( Date: Fri, 30 Jan 2026 13:45:01 +0100 Subject: [PATCH 5/7] feat: Clear previously rendered event dots before re-rendering in CalendarMonth component --- .../ui-kit/src/components/calendar/CalendarMonth.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx index 102083316..f8b932307 100644 --- a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx +++ b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx @@ -69,6 +69,11 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { const cal = document.querySelector(`#cal-${widgetHash}`); if (!cal) return; + // Clear any previously rendered dots before re-rendering + cal.querySelectorAll(`.${DOT_CLASS.split(" ")[0]}`).forEach((el) => + el.remove() + ); + // Build day cell lookup map once (max 42 cells in a month grid) const dayCellMap = new Map(); cal.querySelectorAll(".fc-daygrid-day").forEach((cell) => { @@ -118,9 +123,8 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { const fragment = document.createDocumentFragment(); const seen = new Set(); - dots.filter( - (dot) => !seen.has(dot.id) && seen.size < MAX_DOTS_PER_DAY - ).forEach((dot) => { + dots.forEach((dot) => { + if (seen.has(dot.id) || seen.size >= MAX_DOTS_PER_DAY) return; seen.add(dot.id); const span = document.createElement("span"); if (dot.color) span.style.backgroundColor = dot.color; From 460cd834b24321592d7eef7323e1448ca6eca6a0 Mon Sep 17 00:00:00 2001 From: Xharles Date: Fri, 30 Jan 2026 13:55:07 +0100 Subject: [PATCH 6/7] feat: Enhance event dot rendering by clearing "+X more" links and adding unique count display --- .../src/components/calendar/CalendarMonth.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx index f8b932307..4486e1f5e 100644 --- a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx +++ b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx @@ -69,10 +69,10 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { const cal = document.querySelector(`#cal-${widgetHash}`); if (!cal) return; - // Clear any previously rendered dots before re-rendering - cal.querySelectorAll(`.${DOT_CLASS.split(" ")[0]}`).forEach((el) => - el.remove() - ); + // Clear any previously rendered dots and "+X more" links before re-rendering + cal.querySelectorAll( + `.${DOT_CLASS.split(" ")[0]}, .fc-daygrid-more-link` + ).forEach((el) => el.remove()); // Build day cell lookup map once (max 42 cells in a month grid) const dayCellMap = new Map(); @@ -123,6 +123,13 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { const fragment = document.createDocumentFragment(); const seen = new Set(); + dots.forEach((dot) => { + if (!seen.has(dot.id)) seen.add(dot.id); + }); + + const uniqueCount = seen.size; + seen.clear(); + dots.forEach((dot) => { if (seen.has(dot.id) || seen.size >= MAX_DOTS_PER_DAY) return; seen.add(dot.id); @@ -132,6 +139,13 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { fragment.appendChild(span); }); + if (uniqueCount > MAX_DOTS_PER_DAY) { + const moreEl = document.createElement("span"); + moreEl.className = "fc-daygrid-more-link fc-more-link text-[11px]"; + moreEl.textContent = `+${uniqueCount - MAX_DOTS_PER_DAY} more`; + fragment.appendChild(moreEl); + } + container.appendChild(fragment); }); }; From 07d6505df53b6cc575bf87c83d5a01093c2524de Mon Sep 17 00:00:00 2001 From: Xharles Date: Fri, 30 Jan 2026 14:32:27 +0100 Subject: [PATCH 7/7] feat: Optimize calendar event rendering by replacing moment.js with native Date and improving unique event dot handling --- .../containers/calendar/CalendarContainer.tsx | 47 ++++++++------- .../src/components/calendar/CalendarMonth.tsx | 59 ++++++++++--------- 2 files changed, 57 insertions(+), 49 deletions(-) diff --git a/packages/frontend/src/containers/calendar/CalendarContainer.tsx b/packages/frontend/src/containers/calendar/CalendarContainer.tsx index cb2a4430f..183403bbf 100644 --- a/packages/frontend/src/containers/calendar/CalendarContainer.tsx +++ b/packages/frontend/src/containers/calendar/CalendarContainer.tsx @@ -174,21 +174,32 @@ const CalendarContainer: FC> = ({ const pollingInterval = (moduleData.widget.refresh_interval || POLLING_INTERVAL) * 1000; - const monthStart = moment(selectedDate).startOf("month"); const tagsParam = tags ? filteringListToStr(tags) : undefined; const queryOpts = { skip: !selectedDate, pollingInterval }; + // Compute month boundaries using native Date (immutable — each is a new object) + const monthStartDate = new Date( + selectedDate.getFullYear(), + selectedDate.getMonth(), + 1 + ); + const formatDate = (d: Date) => d.toISOString().slice(0, 10); + const addMonths = (d: Date, n: number) => + new Date(d.getFullYear(), d.getMonth() + n, 1); + + const prevStart = formatDate(addMonths(monthStartDate, -1)); + const currStart = formatDate(monthStartDate); + const nextStart = formatDate(addMonths(monthStartDate, 1)); + const nextEnd = formatDate(addMonths(monthStartDate, 2)); + const { data: prevMonthData, isLoading: isLoadingPrev, isFetching: isFetchingPrev, } = useGetEventsQuery( { - period_after: monthStart - .clone() - .subtract(1, "month") - .format("YYYY-MM-DD"), - period_before: monthStart.format("YYYY-MM-DD"), + period_after: prevStart, + period_before: currStart, limit: QUERY_EVENTS_HARD_LIMIT, tags: tagsParam, }, @@ -201,11 +212,8 @@ const CalendarContainer: FC> = ({ isFetching: isFetchingCurr, } = useGetEventsQuery( { - period_after: monthStart.format("YYYY-MM-DD"), - period_before: monthStart - .clone() - .add(1, "month") - .format("YYYY-MM-DD"), + period_after: currStart, + period_before: nextStart, limit: QUERY_EVENTS_HARD_LIMIT, tags: tagsParam, }, @@ -218,14 +226,8 @@ const CalendarContainer: FC> = ({ isFetching: isFetchingNext, } = useGetEventsQuery( { - period_after: monthStart - .clone() - .add(1, "month") - .format("YYYY-MM-DD"), - period_before: monthStart - .clone() - .add(2, "months") - .format("YYYY-MM-DD"), + period_after: nextStart, + period_before: nextEnd, limit: QUERY_EVENTS_HARD_LIMIT, tags: tagsParam, }, @@ -236,7 +238,12 @@ const CalendarContainer: FC> = ({ const prev = prevMonthData?.results ?? []; const curr = currMonthData?.results ?? []; const next = nextMonthData?.results ?? []; - return [...prev, ...curr, ...next]; + const seen = new Set(); + return [...prev, ...curr, ...next].filter((e) => { + if (seen.has(e.id)) return false; + seen.add(e.id); + return true; + }); }, [ prevMonthData?.results, currMonthData?.results, diff --git a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx index 4486e1f5e..7a0757ded 100644 --- a/packages/ui-kit/src/components/calendar/CalendarMonth.tsx +++ b/packages/ui-kit/src/components/calendar/CalendarMonth.tsx @@ -1,4 +1,4 @@ -import { FC, useRef, useState, useEffect, useCallback } from "react"; +import { FC, useRef, useEffect, useCallback } from "react"; import { ViewContentArg, DatesSetArg } from "@fullcalendar/core"; import dayGridPlugin from "@fullcalendar/daygrid"; import interactionPlugin, { DateClickArg } from "@fullcalendar/interaction"; @@ -99,7 +99,7 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { const current = new Date(startTime); while (current.getTime() < endTime) { - const dateStr = current.toISOString().slice(0, 10); + const dateStr = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-${String(current.getDate()).padStart(2, "0")}`; if (dayCellMap.has(dateStr)) { let dayDots = dotsByDay.get(dateStr); if (!dayDots) { @@ -122,27 +122,25 @@ const renderEventDots = (events: TEvent[], widgetHash: string): void => { const fragment = document.createDocumentFragment(); const seen = new Set(); + let overflow = 0; dots.forEach((dot) => { - if (!seen.has(dot.id)) seen.add(dot.id); - }); - - const uniqueCount = seen.size; - seen.clear(); - - dots.forEach((dot) => { - if (seen.has(dot.id) || seen.size >= MAX_DOTS_PER_DAY) return; + if (seen.has(dot.id)) return; seen.add(dot.id); - const span = document.createElement("span"); - if (dot.color) span.style.backgroundColor = dot.color; - span.className = DOT_CLASS; - fragment.appendChild(span); + if (seen.size <= MAX_DOTS_PER_DAY) { + const span = document.createElement("span"); + if (dot.color) span.style.backgroundColor = dot.color; + span.className = DOT_CLASS; + fragment.appendChild(span); + } else { + overflow += 1; + } }); - if (uniqueCount > MAX_DOTS_PER_DAY) { + if (overflow > 0) { const moreEl = document.createElement("span"); moreEl.className = "fc-daygrid-more-link fc-more-link text-[11px]"; - moreEl.textContent = `+${uniqueCount - MAX_DOTS_PER_DAY} more`; + moreEl.textContent = `+${overflow} more`; fragment.appendChild(moreEl); } @@ -179,7 +177,7 @@ export const CalendarMonth: FC = ({ events, onDatesSet, showFullSize, - catFilters, + catFilters: _catFilters, selectedDate, widgetHash, handleHeaderTooltips, @@ -188,9 +186,9 @@ export const CalendarMonth: FC = ({ isAlphaModalOpen, handleIsAlphaModalOpen, }) => { - const [key, setKey] = useState(0); // to force calendar to rerender - const calendarRef = useRef(null); + const eventsRef = useRef(events); + eventsRef.current = events; const handleSize = (event: ViewContentArg): void => { const contentAPi = event.view.calendar; @@ -335,6 +333,10 @@ export const CalendarMonth: FC = ({ const handleNewMonthView = useCallback( (info: DatesSetArg) => { handleHeaderTooltips(info, widgetHash, showFullSize); + // Re-render dots after FC has swapped grid cells for the new month + if (eventsRef.current?.length) { + renderEventDots(eventsRef.current, widgetHash); + } if (onDatesSet != null) { onDatesSet(info.view.currentStart.toString()); } @@ -399,32 +401,31 @@ export const CalendarMonth: FC = ({ n2?.setAttribute("data-tip", "Next Week"); n2?.setAttribute("data-place", "right"); } + + // Render dots on initial mount when events are already available + if (eventsRef.current?.length) { + renderEventDots(eventsRef.current, widgetHash); + } } }, [widgetHash] ); - // Force remount when events or filters change - useEffect(() => { - setKey((prev) => prev + 1); - }, [events, catFilters]); - - // Batch-render dots after FullCalendar has mounted into the DOM. - // We use requestAnimationFrame to ensure the calendar grid cells - // exist before we query them. + // Re-render dots when event data changes (e.g., new data from queries). + // viewDidMount and datesSet handle lifecycle-driven rendering; + // this effect handles data-driven updates when the grid is already stable. useEffect(() => { if (!events?.length) return undefined; const rafId = requestAnimationFrame(() => { renderEventDots(events, widgetHash); }); return () => cancelAnimationFrame(rafId); - }, [key, events, widgetHash]); + }, [events, widgetHash]); return (