✨(front) improve Scheduler UX and performance
- Enable sticky header by setting height: 100% on .ec - Scroll to current time on calendar initialization - Parallelize CalDAV event fetching with Promise.all - Use event-handler-refs pattern for stable callbacks - Import shared CalendarApi interface from types Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,18 +18,10 @@ import type { EventCalendarAdapter, CalDavExtendedProps } from "../../../service
|
|||||||
import type { CalDavService } from "../../../services/dav/CalDavService";
|
import type { CalDavService } from "../../../services/dav/CalDavService";
|
||||||
import type { CalDavCalendar } from "../../../services/dav/types/caldav-service";
|
import type { CalDavCalendar } from "../../../services/dav/types/caldav-service";
|
||||||
import type { EventCalendarEvent, EventCalendarFetchInfo } from "../../../services/dav/types/event-calendar";
|
import type { EventCalendarEvent, EventCalendarFetchInfo } from "../../../services/dav/types/event-calendar";
|
||||||
|
import type { CalendarApi } from "../types";
|
||||||
|
|
||||||
type ECEvent = EventCalendarEvent;
|
type ECEvent = EventCalendarEvent;
|
||||||
|
|
||||||
// Calendar API interface
|
|
||||||
interface CalendarApi {
|
|
||||||
updateEvent: (event: ECEvent) => void;
|
|
||||||
addEvent: (event: ECEvent) => void;
|
|
||||||
unselect: () => void;
|
|
||||||
refetchEvents: () => void;
|
|
||||||
$destroy?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseSchedulerInitProps {
|
interface UseSchedulerInitProps {
|
||||||
containerRef: MutableRefObject<HTMLDivElement | null>;
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
calendarRef: MutableRefObject<CalendarApi | null>;
|
calendarRef: MutableRefObject<CalendarApi | null>;
|
||||||
@@ -47,6 +39,12 @@ interface UseSchedulerInitProps {
|
|||||||
handleSelect: (info: unknown) => void;
|
handleSelect: (info: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to get current time as HH:MM string
|
||||||
|
const getCurrentTimeString = (): string => {
|
||||||
|
const now = new Date();
|
||||||
|
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const useSchedulerInit = ({
|
export const useSchedulerInit = ({
|
||||||
containerRef,
|
containerRef,
|
||||||
calendarRef,
|
calendarRef,
|
||||||
@@ -66,6 +64,32 @@ export const useSchedulerInit = ({
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { calendarLocale, firstDayOfWeek, formatDayHeader } = useCalendarLocale();
|
const { calendarLocale, firstDayOfWeek, formatDayHeader } = useCalendarLocale();
|
||||||
|
|
||||||
|
// Capture initial scroll time only once on first render
|
||||||
|
const initialScrollTimeRef = useRef<string>(getCurrentTimeString());
|
||||||
|
|
||||||
|
// Store event handlers in refs for stable references (advanced-event-handler-refs pattern)
|
||||||
|
// This prevents calendar recreation when handlers change (e.g., when modalState changes)
|
||||||
|
const handlersRef = useRef({
|
||||||
|
handleEventClick,
|
||||||
|
handleEventDrop,
|
||||||
|
handleEventResize,
|
||||||
|
handleDateClick,
|
||||||
|
handleSelect,
|
||||||
|
setCurrentDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update refs when handlers change (no effect dependencies = no calendar recreation)
|
||||||
|
useEffect(() => {
|
||||||
|
handlersRef.current = {
|
||||||
|
handleEventClick,
|
||||||
|
handleEventDrop,
|
||||||
|
handleEventResize,
|
||||||
|
handleDateClick,
|
||||||
|
handleSelect,
|
||||||
|
setCurrentDate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || calendarRef.current || !isConnected) return;
|
if (!containerRef.current || calendarRef.current || !isConnected) return;
|
||||||
|
|
||||||
@@ -82,7 +106,7 @@ export const useSchedulerInit = ({
|
|||||||
locale: calendarLocale,
|
locale: calendarLocale,
|
||||||
firstDay: firstDayOfWeek,
|
firstDay: firstDayOfWeek,
|
||||||
slotDuration: "00:30",
|
slotDuration: "00:30",
|
||||||
scrollTime: "08:00",
|
scrollTime: initialScrollTimeRef.current,
|
||||||
displayEventEnd: true,
|
displayEventEnd: true,
|
||||||
|
|
||||||
// Interactive features
|
// Interactive features
|
||||||
@@ -96,16 +120,16 @@ export const useSchedulerInit = ({
|
|||||||
selectBackgroundColor: '#ffcdd2', // Light red color for selection
|
selectBackgroundColor: '#ffcdd2', // Light red color for selection
|
||||||
|
|
||||||
// Event handlers - ALL INTERACTIONS
|
// Event handlers - ALL INTERACTIONS
|
||||||
// Cast handlers to bypass library type differences (DomEvent vs MouseEvent)
|
// Use ref wrappers for stable references (prevents calendar recreation on handler changes)
|
||||||
eventClick: handleEventClick as (info: unknown) => void,
|
eventClick: (info: unknown) => handlersRef.current.handleEventClick(info),
|
||||||
eventDrop: handleEventDrop as (info: unknown) => void,
|
eventDrop: (info: unknown) => handlersRef.current.handleEventDrop(info),
|
||||||
eventResize: handleEventResize as (info: unknown) => void,
|
eventResize: (info: unknown) => handlersRef.current.handleEventResize(info),
|
||||||
dateClick: handleDateClick as (info: unknown) => void,
|
dateClick: (info: unknown) => handlersRef.current.handleDateClick(info),
|
||||||
select: handleSelect as (info: unknown) => void,
|
select: (info: unknown) => handlersRef.current.handleSelect(info),
|
||||||
|
|
||||||
// Sync current date with MiniCalendar when navigating
|
// Sync current date with MiniCalendar when navigating
|
||||||
datesSet: (info: { start: Date; end: Date }) => {
|
datesSet: (info: { start: Date; end: Date }) => {
|
||||||
setCurrentDate(info);
|
handlersRef.current.setCurrentDate(info);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Event display
|
// Event display
|
||||||
@@ -133,29 +157,16 @@ export const useSchedulerInit = ({
|
|||||||
try {
|
try {
|
||||||
// Fetch events from ALL calendars in parallel
|
// Fetch events from ALL calendars in parallel
|
||||||
const allEventsPromises = calendars.map(async (calendar) => {
|
const allEventsPromises = calendars.map(async (calendar) => {
|
||||||
// Fetch source events (with recurrence rules) without expansion
|
const timeRange = {
|
||||||
const sourceEventsResult = await caldavService.fetchEvents(
|
start: fetchInfo.start,
|
||||||
calendar.url,
|
end: fetchInfo.end,
|
||||||
{
|
};
|
||||||
timeRange: {
|
|
||||||
start: fetchInfo.start,
|
|
||||||
end: fetchInfo.end,
|
|
||||||
},
|
|
||||||
expand: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch expanded instances
|
// Fetch source events and expanded instances in parallel
|
||||||
const expandedEventsResult = await caldavService.fetchEvents(
|
const [sourceEventsResult, expandedEventsResult] = await Promise.all([
|
||||||
calendar.url,
|
caldavService.fetchEvents(calendar.url, { timeRange, expand: false }),
|
||||||
{
|
caldavService.fetchEvents(calendar.url, { timeRange, expand: true }),
|
||||||
timeRange: {
|
]);
|
||||||
start: fetchInfo.start,
|
|
||||||
end: fetchInfo.end,
|
|
||||||
},
|
|
||||||
expand: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!expandedEventsResult.success || !expandedEventsResult.data) {
|
if (!expandedEventsResult.success || !expandedEventsResult.data) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -241,23 +252,17 @@ export const useSchedulerInit = ({
|
|||||||
containerRef.current.innerHTML = '';
|
containerRef.current.innerHTML = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Note: refs (containerRef, calendarRef, visibleCalendarUrlsRef, davCalendarsRef) are excluded
|
// Note: refs (containerRef, calendarRef, visibleCalendarUrlsRef, davCalendarsRef, initialScrollTimeRef, handlersRef)
|
||||||
// from dependencies as they are stable references that don't trigger re-renders
|
// are excluded from dependencies as they are stable references that don't trigger re-renders.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// Event handlers are accessed via handlersRef to prevent calendar recreation on handler changes.
|
||||||
}, [
|
}, [
|
||||||
isConnected,
|
isConnected,
|
||||||
calendarUrl,
|
calendarUrl,
|
||||||
calendarLocale,
|
calendarLocale,
|
||||||
firstDayOfWeek,
|
firstDayOfWeek,
|
||||||
formatDayHeader,
|
formatDayHeader,
|
||||||
handleEventClick,
|
|
||||||
handleEventDrop,
|
|
||||||
handleEventResize,
|
|
||||||
handleDateClick,
|
|
||||||
handleSelect,
|
|
||||||
caldavService,
|
caldavService,
|
||||||
adapter,
|
adapter,
|
||||||
setCurrentDate,
|
|
||||||
t,
|
t,
|
||||||
i18n.language,
|
i18n.language,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
// 1. CSS Variables - Map --ec-* to Cunningham tokens
|
// 1. CSS Variables - Map --ec-* to Cunningham tokens
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
.ec {
|
.ec {
|
||||||
|
// Fill parent container to enable proper scrolling and sticky header
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
// Base colors
|
// Base colors
|
||||||
--ec-bg-color: var(--c--globals--colors--gray-000);
|
--ec-bg-color: var(--c--globals--colors--gray-000);
|
||||||
--ec-day-bg-color: var(--c--contextuals--background--surface--secondary);
|
--ec-day-bg-color: var(--c--contextuals--background--surface--secondary);
|
||||||
|
|||||||
Reference in New Issue
Block a user