♻️(front) integrate custom toolbar in Scheduler
- Integrate SchedulerToolbar component above calendar container - Disable native headerToolbar in useSchedulerInit - Add toolbar state (currentView, viewTitle) to Scheduler - Use datesSet callback to sync toolbar with calendar navigation - Update CalendarContext to use CalendarApi type for better type safety - Pass calendarRef to toolbar for API method access Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
* - Click to edit (eventClick)
|
||||
* - Click to create (dateClick)
|
||||
* - Select range to create (select)
|
||||
* - Custom toolbar with navigation and view selection
|
||||
*
|
||||
* Next.js consideration: This component must be client-side only
|
||||
* due to DOM manipulation. Use dynamic import with ssr: false if needed.
|
||||
@@ -15,13 +16,13 @@
|
||||
|
||||
import "@event-calendar/core/index.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useCalendarContext } from "../../contexts/CalendarContext";
|
||||
import type { CalDavCalendar } from "../../services/dav/types/caldav-service";
|
||||
import type { EventCalendarEvent } from "../../services/dav/types/event-calendar";
|
||||
|
||||
import { EventModal } from "./EventModal";
|
||||
import { SchedulerToolbar } from "./SchedulerToolbar";
|
||||
import type { SchedulerProps, EventModalState } from "./types";
|
||||
import { useSchedulerHandlers } from "./hooks/useSchedulerHandlers";
|
||||
import {
|
||||
@@ -29,17 +30,6 @@ import {
|
||||
useSchedulingCapabilitiesCheck,
|
||||
} from "./hooks/useSchedulerInit";
|
||||
|
||||
type ECEvent = EventCalendarEvent;
|
||||
|
||||
// Calendar API interface
|
||||
interface CalendarApi {
|
||||
updateEvent: (event: ECEvent) => void;
|
||||
addEvent: (event: ECEvent) => void;
|
||||
unselect: () => void;
|
||||
refetchEvents: () => void;
|
||||
$destroy?: () => void;
|
||||
}
|
||||
|
||||
export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
const {
|
||||
caldavService,
|
||||
@@ -52,9 +42,13 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
} = useCalendarContext();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const calendarRef = contextCalendarRef as React.MutableRefObject<CalendarApi | null>;
|
||||
const calendarRef = contextCalendarRef;
|
||||
const [calendarUrl, setCalendarUrl] = useState(defaultCalendarUrl || "");
|
||||
|
||||
// Toolbar state
|
||||
const [currentView, setCurrentView] = useState("timeGridWeek");
|
||||
const [viewTitle, setViewTitle] = useState("");
|
||||
|
||||
// Modal state
|
||||
const [modalState, setModalState] = useState<EventModalState>({
|
||||
isOpen: false,
|
||||
@@ -102,6 +96,25 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
setModalState,
|
||||
});
|
||||
|
||||
// Callback to update toolbar state when calendar dates/view changes
|
||||
const handleDatesSet = useCallback(
|
||||
(info: { start: Date; end: Date; view?: { type: string; title: string } }) => {
|
||||
// Update current date for MiniCalendar sync
|
||||
const midTime = (info.start.getTime() + info.end.getTime()) / 2;
|
||||
setCurrentDate(new Date(midTime));
|
||||
|
||||
// Update toolbar state
|
||||
if (calendarRef.current) {
|
||||
const view = calendarRef.current.getView();
|
||||
if (view) {
|
||||
setCurrentView(view.type);
|
||||
setViewTitle(view.title);
|
||||
}
|
||||
}
|
||||
},
|
||||
[setCurrentDate, calendarRef]
|
||||
);
|
||||
|
||||
// Initialize calendar
|
||||
// Cast handlers to bypass library type differences between specific event types and unknown
|
||||
useSchedulerInit({
|
||||
@@ -113,7 +126,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
adapter,
|
||||
visibleCalendarUrlsRef,
|
||||
davCalendarsRef,
|
||||
setCurrentDate,
|
||||
setCurrentDate: handleDatesSet,
|
||||
handleEventClick: handleEventClick as (info: unknown) => void,
|
||||
handleEventDrop: handleEventDrop as unknown as (info: unknown) => void,
|
||||
handleEventResize: handleEventResize as unknown as (info: unknown) => void,
|
||||
@@ -121,6 +134,17 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
handleSelect: handleSelect as (info: unknown) => void,
|
||||
});
|
||||
|
||||
// Update toolbar title on initial render
|
||||
useEffect(() => {
|
||||
if (calendarRef.current) {
|
||||
const view = calendarRef.current.getView();
|
||||
if (view) {
|
||||
setCurrentView(view.type);
|
||||
setViewTitle(view.title);
|
||||
}
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
// Update eventFilter when visible calendars change
|
||||
useEffect(() => {
|
||||
if (calendarRef.current) {
|
||||
@@ -130,12 +154,24 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
}
|
||||
}, [visibleCalendarUrls, davCalendars]);
|
||||
|
||||
const handleViewChange = useCallback((view: string) => {
|
||||
setCurrentView(view);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="scheduler">
|
||||
<SchedulerToolbar
|
||||
calendarRef={calendarRef}
|
||||
currentView={currentView}
|
||||
viewTitle={viewTitle}
|
||||
onViewChange={handleViewChange}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="event-calendar"
|
||||
style={{ height: "calc(100vh - 100px)" }}
|
||||
className="scheduler__calendar"
|
||||
style={{ height: "calc(100vh - 160px)" }}
|
||||
/>
|
||||
|
||||
<EventModal
|
||||
@@ -150,7 +186,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
onRespondToInvitation={handleRespondToInvitation}
|
||||
onClose={handleModalClose}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ interface UseSchedulerInitProps {
|
||||
adapter: EventCalendarAdapter;
|
||||
visibleCalendarUrlsRef: MutableRefObject<Set<string>>;
|
||||
davCalendarsRef: MutableRefObject<CalDavCalendar[]>;
|
||||
setCurrentDate: (date: Date) => void;
|
||||
setCurrentDate: (info: { start: Date; end: Date }) => void;
|
||||
handleEventClick: (info: unknown) => void;
|
||||
handleEventDrop: (info: unknown) => void;
|
||||
handleEventResize: (info: unknown) => void;
|
||||
@@ -75,20 +75,8 @@ export const useSchedulerInit = ({
|
||||
{
|
||||
// View configuration
|
||||
view: "timeGridWeek",
|
||||
headerToolbar: {
|
||||
start: "prev,next today",
|
||||
center: "title",
|
||||
end: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
|
||||
},
|
||||
|
||||
// Button text translations
|
||||
buttonText: {
|
||||
today: t('calendar.views.today'),
|
||||
dayGridMonth: t('calendar.views.month'),
|
||||
timeGridWeek: t('calendar.views.week'),
|
||||
timeGridDay: t('calendar.views.day'),
|
||||
listWeek: t('calendar.views.listWeek'),
|
||||
},
|
||||
// Native toolbar disabled - using custom React toolbar (SchedulerToolbar)
|
||||
headerToolbar: false,
|
||||
|
||||
// Locale & time settings
|
||||
locale: calendarLocale,
|
||||
@@ -117,9 +105,7 @@ export const useSchedulerInit = ({
|
||||
|
||||
// Sync current date with MiniCalendar when navigating
|
||||
datesSet: (info: { start: Date; end: Date }) => {
|
||||
// Use the middle of the visible range as the "current" date
|
||||
const midTime = (info.start.getTime() + info.end.getTime()) / 2;
|
||||
setCurrentDate(new Date(midTime));
|
||||
setCurrentDate(info);
|
||||
},
|
||||
|
||||
// Event display
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { createContext, useContext, useRef, useMemo, useState, useEffect, useCallback, type ReactNode } from "react";
|
||||
import { Calendar } from "@event-calendar/core";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useRef,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { CalDavService } from "../services/dav/CalDavService";
|
||||
import { EventCalendarAdapter } from "../services/dav/EventCalendarAdapter";
|
||||
import { caldavServerUrl, headers, fetchOptions } from "../utils/DavClient";
|
||||
import type { CalDavCalendar, CalDavCalendarCreate } from "../services/dav/types/caldav-service";
|
||||
import type {
|
||||
CalDavCalendar,
|
||||
CalDavCalendarCreate,
|
||||
} from "../services/dav/types/caldav-service";
|
||||
import type { CalendarApi } from "../components/scheduler/types";
|
||||
import { createCalendarApi } from "../api";
|
||||
|
||||
export interface CalendarContextType {
|
||||
calendarRef: React.RefObject<Calendar | null>;
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
caldavService: CalDavService;
|
||||
adapter: EventCalendarAdapter;
|
||||
davCalendars: CalDavCalendar[];
|
||||
@@ -20,19 +32,33 @@ export interface CalendarContextType {
|
||||
setSelectedDate: (date: Date) => void;
|
||||
refreshCalendars: () => Promise<void>;
|
||||
toggleCalendarVisibility: (calendarUrl: string) => void;
|
||||
createCalendar: (params: CalDavCalendarCreate) => Promise<{ success: boolean; error?: string }>;
|
||||
updateCalendar: (calendarUrl: string, params: { displayName?: string; color?: string; description?: string }) => Promise<{ success: boolean; error?: string }>;
|
||||
deleteCalendar: (calendarUrl: string) => Promise<{ success: boolean; error?: string }>;
|
||||
shareCalendar: (calendarUrl: string, email: string) => Promise<{ success: boolean; error?: string }>;
|
||||
createCalendar: (
|
||||
params: CalDavCalendarCreate,
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
updateCalendar: (
|
||||
calendarUrl: string,
|
||||
params: { displayName?: string; color?: string; description?: string },
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
deleteCalendar: (
|
||||
calendarUrl: string,
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
shareCalendar: (
|
||||
calendarUrl: string,
|
||||
email: string,
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
goToDate: (date: Date) => void;
|
||||
}
|
||||
|
||||
const CalendarContext = createContext<CalendarContextType | undefined>(undefined);
|
||||
const CalendarContext = createContext<CalendarContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const useCalendarContext = () => {
|
||||
const context = useContext(CalendarContext);
|
||||
if (!context) {
|
||||
throw new Error("useCalendarContext must be used within a CalendarContextProvider");
|
||||
throw new Error(
|
||||
"useCalendarContext must be used within a CalendarContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -41,12 +67,16 @@ interface CalendarContextProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const CalendarContextProvider = ({ children }: CalendarContextProviderProps) => {
|
||||
const calendarRef = useRef<Calendar | null>(null);
|
||||
export const CalendarContextProvider = ({
|
||||
children,
|
||||
}: CalendarContextProviderProps) => {
|
||||
const calendarRef = useRef<CalendarApi | null>(null);
|
||||
const caldavService = useMemo(() => new CalDavService(), []);
|
||||
const adapter = useMemo(() => new EventCalendarAdapter(), []);
|
||||
const [davCalendars, setDavCalendars] = useState<CalDavCalendar[]>([]);
|
||||
const [visibleCalendarUrls, setVisibleCalendarUrls] = useState<Set<string>>(new Set());
|
||||
const [visibleCalendarUrls, setVisibleCalendarUrls] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
@@ -59,7 +89,7 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
if (result.success && result.data) {
|
||||
setDavCalendars(result.data);
|
||||
// Initialize all calendars as visible
|
||||
setVisibleCalendarUrls(new Set(result.data.map(cal => cal.url)));
|
||||
setVisibleCalendarUrls(new Set(result.data.map((cal) => cal.url)));
|
||||
} else {
|
||||
console.error("Error fetching calendars:", result.error);
|
||||
setDavCalendars([]);
|
||||
@@ -75,7 +105,7 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
}, [caldavService]);
|
||||
|
||||
const toggleCalendarVisibility = useCallback((calendarUrl: string) => {
|
||||
setVisibleCalendarUrls(prev => {
|
||||
setVisibleCalendarUrls((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(calendarUrl)) {
|
||||
newSet.delete(calendarUrl);
|
||||
@@ -86,85 +116,124 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createCalendar = useCallback(async (params: CalDavCalendarCreate): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
// Use Django API to create calendar (creates both CalDAV and Django records)
|
||||
await createCalendarApi({
|
||||
name: params.displayName,
|
||||
color: params.color,
|
||||
description: params.description,
|
||||
});
|
||||
// Refresh CalDAV calendars list to show the new calendar
|
||||
await refreshCalendars();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error creating calendar:", error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
}, [refreshCalendars]);
|
||||
|
||||
const updateCalendar = useCallback(async (
|
||||
calendarUrl: string,
|
||||
params: { displayName?: string; color?: string; description?: string }
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await caldavService.updateCalendar(calendarUrl, params);
|
||||
if (result.success) {
|
||||
await refreshCalendars();
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || 'Failed to update calendar' };
|
||||
} catch (error) {
|
||||
console.error("Error updating calendar:", error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
}, [caldavService, refreshCalendars]);
|
||||
|
||||
const deleteCalendar = useCallback(async (calendarUrl: string): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await caldavService.deleteCalendar(calendarUrl);
|
||||
if (result.success) {
|
||||
// Remove from visible calendars
|
||||
setVisibleCalendarUrls(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(calendarUrl);
|
||||
return newSet;
|
||||
const createCalendar = useCallback(
|
||||
async (
|
||||
params: CalDavCalendarCreate,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
// Use Django API to create calendar (creates both CalDAV and Django records)
|
||||
await createCalendarApi({
|
||||
name: params.displayName,
|
||||
color: params.color,
|
||||
description: params.description,
|
||||
});
|
||||
// Refresh CalDAV calendars list to show the new calendar
|
||||
await refreshCalendars();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error creating calendar:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
return { success: false, error: result.error || 'Failed to delete calendar' };
|
||||
} catch (error) {
|
||||
console.error("Error deleting calendar:", error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
}, [caldavService, refreshCalendars]);
|
||||
},
|
||||
[refreshCalendars],
|
||||
);
|
||||
|
||||
const shareCalendar = useCallback(async (
|
||||
calendarUrl: string,
|
||||
email: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await caldavService.shareCalendar({
|
||||
calendarUrl,
|
||||
sharees: [{
|
||||
href: `mailto:${email}`,
|
||||
privilege: 'read-write', // Same rights as principal
|
||||
}],
|
||||
});
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
const updateCalendar = useCallback(
|
||||
async (
|
||||
calendarUrl: string,
|
||||
params: { displayName?: string; color?: string; description?: string },
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await caldavService.updateCalendar(calendarUrl, params);
|
||||
if (result.success) {
|
||||
await refreshCalendars();
|
||||
return { success: true };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || "Failed to update calendar",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating calendar:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
return { success: false, error: result.error || 'Failed to share calendar' };
|
||||
} catch (error) {
|
||||
console.error("Error sharing calendar:", error);
|
||||
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
|
||||
}
|
||||
}, [caldavService]);
|
||||
},
|
||||
[caldavService, refreshCalendars],
|
||||
);
|
||||
|
||||
const deleteCalendar = useCallback(
|
||||
async (
|
||||
calendarUrl: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await caldavService.deleteCalendar(calendarUrl);
|
||||
if (result.success) {
|
||||
// Remove from visible calendars
|
||||
setVisibleCalendarUrls((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(calendarUrl);
|
||||
return newSet;
|
||||
});
|
||||
await refreshCalendars();
|
||||
return { success: true };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || "Failed to delete calendar",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting calendar:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
},
|
||||
[caldavService, refreshCalendars],
|
||||
);
|
||||
|
||||
const shareCalendar = useCallback(
|
||||
async (
|
||||
calendarUrl: string,
|
||||
email: string,
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await caldavService.shareCalendar({
|
||||
calendarUrl,
|
||||
sharees: [
|
||||
{
|
||||
href: `mailto:${email}`,
|
||||
privilege: "read-write", // Same rights as principal
|
||||
},
|
||||
],
|
||||
});
|
||||
if (result.success) {
|
||||
return { success: true };
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || "Failed to share calendar",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error sharing calendar:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
},
|
||||
[caldavService],
|
||||
);
|
||||
|
||||
const goToDate = useCallback((date: Date) => {
|
||||
if (calendarRef.current) {
|
||||
calendarRef.current.setOption('date', date);
|
||||
calendarRef.current.setOption("date", date);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -187,7 +256,9 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
const calendarsResult = await caldavService.fetchCalendars();
|
||||
if (isMounted && calendarsResult.success && calendarsResult.data) {
|
||||
setDavCalendars(calendarsResult.data);
|
||||
setVisibleCalendarUrls(new Set(calendarsResult.data.map(cal => cal.url)));
|
||||
setVisibleCalendarUrls(
|
||||
new Set(calendarsResult.data.map((cal) => cal.url)),
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (isMounted) {
|
||||
@@ -210,7 +281,6 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
};
|
||||
// Note: refreshCalendars is excluded to avoid dependency cycle
|
||||
// The initial fetch is done inline in this effect
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [caldavService]);
|
||||
|
||||
const value: CalendarContextType = {
|
||||
@@ -234,5 +304,9 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
goToDate,
|
||||
};
|
||||
|
||||
return <CalendarContext.Provider value={value}>{children}</CalendarContext.Provider>;
|
||||
return (
|
||||
<CalendarContext.Provider value={value}>
|
||||
{children}
|
||||
</CalendarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user