From cb07b7738983ff54141c22eec8a34bc7831582a4 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Sun, 25 Jan 2026 20:34:09 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(front)=20add=20calendar=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add useCalendars hook for CalDAV calendar management and useCalendarLocale for locale-aware date formatting. Remove obsolete modal hooks replaced by Scheduler. Co-Authored-By: Claude Opus 4.5 --- .../calendar/hooks/useCalendarLocale.ts | 140 ++++++++++++++++++ .../features/calendar/hooks/useCalendars.ts | 86 ++++++++++- .../calendar/hooks/useCreateEventModal.tsx | 45 ------ .../features/calendar/hooks/useEventModal.tsx | 102 ------------- 4 files changed, 224 insertions(+), 149 deletions(-) create mode 100644 src/frontend/apps/calendars/src/features/calendar/hooks/useCalendarLocale.ts delete mode 100644 src/frontend/apps/calendars/src/features/calendar/hooks/useCreateEventModal.tsx delete mode 100644 src/frontend/apps/calendars/src/features/calendar/hooks/useEventModal.tsx diff --git a/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendarLocale.ts b/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendarLocale.ts new file mode 100644 index 0000000..de22074 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendarLocale.ts @@ -0,0 +1,140 @@ +/** + * Hook for calendar locale management + * + * Provides locale-aware utilities for date formatting and calendar configuration + */ +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { enUS, fr, nl, Locale } from 'date-fns/locale'; + +// Map i18n language codes to date-fns locales +const DATE_FNS_LOCALES: Record = { + en: enUS, + 'en-us': enUS, + 'en-US': enUS, + fr: fr, + 'fr-fr': fr, + 'fr-FR': fr, + nl: nl, + 'nl-nl': nl, + 'nl-NL': nl, +}; + +// Map i18n language codes to Intl locale codes +const INTL_LOCALES: Record = { + en: 'en-US', + 'en-us': 'en-US', + 'en-US': 'en-US', + fr: 'fr-FR', + 'fr-fr': 'fr-FR', + 'fr-FR': 'fr-FR', + nl: 'nl-NL', + 'nl-nl': 'nl-NL', + 'nl-NL': 'nl-NL', +}; + +// First day of week by locale (0 = Sunday, 1 = Monday) +const FIRST_DAY_OF_WEEK: Record = { + en: 0, // US: Sunday + 'en-us': 0, + 'en-US': 0, + fr: 1, // France: Monday + 'fr-fr': 1, + 'fr-FR': 1, + nl: 1, // Netherlands: Monday + 'nl-nl': 1, + 'nl-NL': 1, +}; + +export function useCalendarLocale() { + const { i18n, t } = useTranslation(); + const currentLanguage = i18n.language; + + // Get date-fns locale + const dateFnsLocale = useMemo(() => { + return DATE_FNS_LOCALES[currentLanguage] || DATE_FNS_LOCALES['en'] || enUS; + }, [currentLanguage]); + + // Get Intl locale string (e.g., 'fr-FR') + const intlLocale = useMemo(() => { + return INTL_LOCALES[currentLanguage] || INTL_LOCALES['en'] || 'en-US'; + }, [currentLanguage]); + + // Get calendar library locale code + const calendarLocale = useMemo(() => { + const base = currentLanguage.split('-')[0]; + return base || 'en'; + }, [currentLanguage]); + + // Get first day of week for this locale + const firstDayOfWeek = useMemo(() => { + return FIRST_DAY_OF_WEEK[currentLanguage] ?? FIRST_DAY_OF_WEEK['en'] ?? 0; + }, [currentLanguage]); + + // Format date using Intl.DateTimeFormat with current locale + const formatDate = useCallback(( + date: Date | string, + options?: Intl.DateTimeFormatOptions + ): string => { + const d = typeof date === 'string' ? new Date(date) : date; + return new Intl.DateTimeFormat(intlLocale, options).format(d); + }, [intlLocale]); + + // Format time using Intl.DateTimeFormat with current locale + const formatTime = useCallback(( + date: Date | string, + options?: Intl.DateTimeFormatOptions + ): string => { + const d = typeof date === 'string' ? new Date(date) : date; + return new Intl.DateTimeFormat(intlLocale, { + hour: 'numeric', + minute: 'numeric', + ...options, + }).format(d); + }, [intlLocale]); + + // Format day header for calendar views + const formatDayHeader = useCallback((date: Date): { html: string } => { + const dayOfWeek = date.toLocaleDateString(intlLocale, { weekday: 'short' }); + const dayOfMonth = date.toLocaleDateString(intlLocale, { day: 'numeric' }); + return { + html: `
${dayOfMonth} ${dayOfWeek}
`, + }; + }, [intlLocale]); + + // Get calendar translations for the library + const getCalendarTranslations = useCallback(() => ({ + dayGridMonth: t('calendar.views.month'), + dayGridWeek: t('calendar.views.week'), + dayGridDay: t('calendar.views.day'), + timeGridWeek: t('calendar.views.week'), + timeGridDay: t('calendar.views.day'), + listDay: t('calendar.views.listDay'), + listWeek: t('calendar.views.listWeek'), + listMonth: t('calendar.views.listMonth'), + listYear: t('calendar.views.listYear'), + today: t('calendar.views.today'), + }), [t]); + + return { + // Locales + dateFnsLocale, + intlLocale, + calendarLocale, + currentLanguage, + + // Calendar settings + firstDayOfWeek, + + // Formatters + formatDate, + formatTime, + formatDayHeader, + + // Translations + getCalendarTranslations, + t, + }; +} + +export type CalendarLocale = ReturnType; diff --git a/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts b/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts index e1742c8..4544bd1 100644 --- a/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts +++ b/src/frontend/apps/calendars/src/features/calendar/hooks/useCalendars.ts @@ -6,8 +6,15 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Calendar, - createCalendar, + createCalendarApi, + createSubscriptionToken, + deleteSubscriptionToken, getCalendars, + getSubscriptionToken, + GetSubscriptionTokenResult, + SubscriptionToken, + SubscriptionTokenError, + SubscriptionTokenParams, toggleCalendarVisibility, } from "../api"; @@ -30,7 +37,7 @@ export const useCreateCalendar = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: createCalendar, + mutationFn: createCalendarApi, onSuccess: () => { queryClient.invalidateQueries({ queryKey: CALENDARS_KEY }); }, @@ -50,3 +57,78 @@ export const useToggleCalendarVisibility = () => { }, }); }; + +/** + * Result type for useSubscriptionToken hook. + */ +export interface UseSubscriptionTokenResult { + token: SubscriptionToken | null; + tokenError: SubscriptionTokenError | null; + isLoading: boolean; + refetch: () => void; +} + +/** + * Hook to get subscription token for a calendar by CalDAV path. + * Handles the result/error pattern from getSubscriptionToken. + */ +export const useSubscriptionToken = (caldavPath: string): UseSubscriptionTokenResult => { + const query = useQuery({ + queryKey: ["subscription-token", caldavPath], + queryFn: () => getSubscriptionToken(caldavPath), + enabled: !!caldavPath, + retry: false, + }); + + // Extract token and error from the result using proper type narrowing + const result = query.data; + let token: SubscriptionToken | null = null; + let tokenError: SubscriptionTokenError | null = null; + + if (result) { + if (result.success) { + token = result.token; + } else { + tokenError = result.error; + } + } + + return { + token, + tokenError, + isLoading: query.isLoading, + refetch: query.refetch, + }; +}; + +/** + * Hook to create a subscription token. + */ +export const useCreateSubscriptionToken = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createSubscriptionToken, + onSuccess: (_data, params: SubscriptionTokenParams) => { + queryClient.invalidateQueries({ + queryKey: ["subscription-token", params.caldavPath], + }); + }, + }); +}; + +/** + * Hook to delete (revoke) a subscription token. + */ +export const useDeleteSubscriptionToken = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteSubscriptionToken, + onSuccess: (_data, caldavPath: string) => { + queryClient.invalidateQueries({ + queryKey: ["subscription-token", caldavPath], + }); + }, + }); +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/hooks/useCreateEventModal.tsx b/src/frontend/apps/calendars/src/features/calendar/hooks/useCreateEventModal.tsx deleted file mode 100644 index 5e5e842..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/hooks/useCreateEventModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Simple wrapper around useEventModal for the "Créer" button. - */ - -import { useMemo } from "react"; -import type { IcsEvent } from 'ts-ics'; -import { useEventModal, apiCalendarToOpenCalendar } from './useEventModal'; -import { Calendar as ApiCalendar } from '../api'; - -interface UseCreateEventModalProps { - calendars?: ApiCalendar[] | null; - selectedDate?: Date; -} - -export const useCreateEventModal = ({ calendars, selectedDate }: UseCreateEventModalProps) => { - const openCalendars = useMemo(() => { - if (!Array.isArray(calendars)) return []; - return calendars.map(apiCalendarToOpenCalendar); - }, [calendars]); - - const initialEvent = useMemo((): IcsEvent | null => { - if (!selectedDate) return null; - const start = new Date(selectedDate); - start.setHours(9, 0, 0, 0); - const end = new Date(start); - end.setHours(10, 0, 0, 0); - return { - uid: `event-${Date.now()}`, - summary: '', - start: { type: 'DATE-TIME', date: start, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }, - end: { type: 'DATE-TIME', date: end, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }, - }; - }, [selectedDate]); - - const modal = useEventModal({ - calendars: openCalendars, - initialEvent, - initialCalendarUrl: Array.isArray(calendars) && calendars[0]?.id ? calendars[0].id : '', - }); - - return { - ...modal, - open: () => modal.open(initialEvent, 'create', Array.isArray(calendars) && calendars[0]?.id ? calendars[0].id : ''), - }; -}; diff --git a/src/frontend/apps/calendars/src/features/calendar/hooks/useEventModal.tsx b/src/frontend/apps/calendars/src/features/calendar/hooks/useEventModal.tsx deleted file mode 100644 index e4762ec..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/hooks/useEventModal.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Unified hook for managing EventModal state. - * Can be used both in React components and adapted for open-calendar. - */ - -import { useState, useMemo, useCallback, useEffect } from "react"; -import type { IcsEvent } from 'ts-ics'; -import { EventModal } from '../components/EventModal'; -import { Calendar as ApiCalendar } from '../api'; - -// Re-export Calendar type from EventModal for consistency -export type { Calendar as OpenCalendar } from '../components/EventModal'; - -// Convert API Calendar to open-calendar Calendar format -export function apiCalendarToOpenCalendar(cal: ApiCalendar): OpenCalendar { - return { - url: cal.id, - uid: cal.id, - displayName: cal.name, - calendarColor: cal.color, - description: cal.description, - }; -} - - -interface UseEventModalProps { - calendars?: OpenCalendar[]; - initialEvent?: IcsEvent | null; - initialCalendarUrl?: string; - onSubmit?: (event: IcsEvent, calendarUrl: string) => Promise; - onDelete?: (event: IcsEvent, calendarUrl: string) => Promise; -} - -export const useEventModal = ({ - calendars, - initialEvent = null, - initialCalendarUrl = '', - onSubmit: customOnSubmit, - onDelete: customOnDelete, -}: UseEventModalProps) => { - const [isOpen, setIsOpen] = useState(false); - const [mode, setMode] = useState<'create' | 'edit'>('create'); - const [event, setEvent] = useState(initialEvent); - const [currentCalendars, setCurrentCalendars] = useState(calendars || []); - const [calendarUrl, setCalendarUrl] = useState(initialCalendarUrl || calendars[0]?.url || ''); - const [allDay, setAllDay] = useState(false); - - // Update calendars when they change - useEffect(() => { - if (calendars && calendars.length > 0) { - setCurrentCalendars(calendars); - } - }, [calendars]); - - const handleSubmit = useCallback(async (event: IcsEvent, calendarUrl: string) => { - if (customOnSubmit) { - await customOnSubmit(event, calendarUrl); - } - setIsOpen(false); - }, [customOnSubmit]); - - const handleDelete = useCallback(async (event: IcsEvent, calendarUrl: string) => { - if (customOnDelete) { - await customOnDelete(event, calendarUrl); - } - setIsOpen(false); - }, [customOnDelete]); - - const open = useCallback((event?: IcsEvent | null, mode: 'create' | 'edit' = 'create', calendarUrl?: string, calendars?: OpenCalendar[]) => { - setEvent(event || null); - setMode(mode); - if (calendarUrl !== undefined) setCalendarUrl(calendarUrl); - if (calendars && calendars.length > 0) setCurrentCalendars(calendars); - setIsOpen(true); - }, []); - - const close = useCallback(() => { - setIsOpen(false); - }, []); - - return { - isOpen, - mode, - event, - calendarUrl, - open, - close, - Modal: ( - - ), - }; -};