From 87995796067265209e0b0bb5909c2170babce937 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Wed, 28 Jan 2026 12:55:55 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(front)=20integrate=20custom?= =?UTF-8?q?=20toolbar=20in=20Scheduler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../components/scheduler/Scheduler.tsx | 72 +++-- .../scheduler/hooks/useSchedulerInit.ts | 22 +- .../calendar/contexts/CalendarContext.tsx | 250 ++++++++++++------ 3 files changed, 220 insertions(+), 124 deletions(-) diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx index b497de4..3200a4f 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx @@ -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(null); - const calendarRef = contextCalendarRef as React.MutableRefObject; + 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({ 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 ( - <> +
+ +
{ onRespondToInvitation={handleRespondToInvitation} onClose={handleModalClose} /> - +
); }; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts index cbec0b0..d9e94b3 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/hooks/useSchedulerInit.ts @@ -39,7 +39,7 @@ interface UseSchedulerInitProps { adapter: EventCalendarAdapter; visibleCalendarUrlsRef: MutableRefObject>; davCalendarsRef: MutableRefObject; - 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 diff --git a/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx b/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx index c345ed5..5349ea6 100644 --- a/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx @@ -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; + calendarRef: React.RefObject; caldavService: CalDavService; adapter: EventCalendarAdapter; davCalendars: CalDavCalendar[]; @@ -20,19 +32,33 @@ export interface CalendarContextType { setSelectedDate: (date: Date) => void; refreshCalendars: () => Promise; 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(undefined); +const CalendarContext = createContext( + 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(null); +export const CalendarContextProvider = ({ + children, +}: CalendarContextProviderProps) => { + const calendarRef = useRef(null); const caldavService = useMemo(() => new CalDavService(), []); const adapter = useMemo(() => new EventCalendarAdapter(), []); const [davCalendars, setDavCalendars] = useState([]); - const [visibleCalendarUrls, setVisibleCalendarUrls] = useState>(new Set()); + const [visibleCalendarUrls, setVisibleCalendarUrls] = useState>( + new Set(), + ); const [isLoading, setIsLoading] = useState(true); const [isConnected, setIsConnected] = useState(false); const [currentDate, setCurrentDate] = useState(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 {children}; + return ( + + {children} + + ); };