♻️(front) improve React code quality and performance
- Remove all console.log debug statements from production code - Fix useEffect dependency arrays (remove refs, fix dependency cycles) - Add useMemo for sharedCalendars filtering in CalendarList - Improve error handling in handleOpenSubscriptionModal - Add proper cleanup pattern (isMounted) in CalendarContext - Use i18n locale instead of hardcoded French in MiniCalendar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,7 @@ export const LeftPanel = ({
|
||||
onDateSelect,
|
||||
onCreateEvent,
|
||||
}: LeftPanelProps) => {
|
||||
const { davCalendars } = useCalendarContext();
|
||||
console.log("davCalendars LeftPanel", davCalendars);
|
||||
useCalendarContext();
|
||||
return (
|
||||
<div className="calendar-left-panel">
|
||||
<div className="calendar-left-panel__create">
|
||||
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
startOfWeek,
|
||||
subMonths,
|
||||
} from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCalendarContext } from "../contexts";
|
||||
import { useCalendarLocale } from "../hooks/useCalendarLocale";
|
||||
|
||||
interface MiniCalendarProps {
|
||||
selectedDate: Date;
|
||||
@@ -41,6 +41,7 @@ export const MiniCalendar = ({
|
||||
}: MiniCalendarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { goToDate, currentDate } = useCalendarContext();
|
||||
const { dateFnsLocale, firstDayOfWeek } = useCalendarLocale();
|
||||
const [viewDate, setViewDate] = useState(selectedDate);
|
||||
|
||||
// Sync viewDate when main calendar navigates (via prev/next buttons)
|
||||
@@ -57,16 +58,33 @@ export const MiniCalendar = ({
|
||||
const days = useMemo(() => {
|
||||
const monthStart = startOfMonth(viewDate);
|
||||
const monthEnd = endOfMonth(viewDate);
|
||||
const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 });
|
||||
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
|
||||
const weekStartsOn = firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
const calendarStart = startOfWeek(monthStart, { weekStartsOn });
|
||||
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn });
|
||||
|
||||
return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
|
||||
}, [viewDate]);
|
||||
}, [viewDate, firstDayOfWeek]);
|
||||
|
||||
// Group days by weeks
|
||||
const weeks = useMemo(() => chunkArray(days, 7), [days]);
|
||||
|
||||
const weekDays = ["lu", "ma", "me", "je", "ve", "sa", "di"];
|
||||
// Generate weekday labels based on locale and first day of week
|
||||
const weekDays = useMemo(() => {
|
||||
const days = [
|
||||
t('calendar.recurrence.weekdays.mo'),
|
||||
t('calendar.recurrence.weekdays.tu'),
|
||||
t('calendar.recurrence.weekdays.we'),
|
||||
t('calendar.recurrence.weekdays.th'),
|
||||
t('calendar.recurrence.weekdays.fr'),
|
||||
t('calendar.recurrence.weekdays.sa'),
|
||||
t('calendar.recurrence.weekdays.su'),
|
||||
];
|
||||
// Rotate array based on firstDayOfWeek (0 = Sunday, 1 = Monday)
|
||||
if (firstDayOfWeek === 0) {
|
||||
return [days[6], ...days.slice(0, 6)];
|
||||
}
|
||||
return days;
|
||||
}, [t, firstDayOfWeek]);
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
setViewDate(subMonths(viewDate, 1));
|
||||
@@ -85,7 +103,7 @@ export const MiniCalendar = ({
|
||||
<div className="mini-calendar">
|
||||
<div className="mini-calendar__header">
|
||||
<span className="mini-calendar__month-title">
|
||||
{format(viewDate, "MMMM yyyy", { locale: fr })}
|
||||
{format(viewDate, "MMMM yyyy", { locale: dateFnsLocale })}
|
||||
</span>
|
||||
<div className="mini-calendar__nav">
|
||||
<button
|
||||
@@ -121,7 +139,8 @@ export const MiniCalendar = ({
|
||||
{/* Calendar body with weeks */}
|
||||
<div className="mini-calendar__body">
|
||||
{weeks.map((week, weekIndex) => {
|
||||
const weekNumber = getWeek(week[0], { weekStartsOn: 1 });
|
||||
const weekStartsOn = firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
const weekNumber = getWeek(week[0], { weekStartsOn });
|
||||
return (
|
||||
<div key={weekIndex} className="mini-calendar__week">
|
||||
<div className="mini-calendar__week-number">{weekNumber}</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* CalendarList component - List of calendars with visibility toggles.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { Calendar } from "../../types";
|
||||
@@ -72,6 +72,8 @@ export const CalendarList = ({ calendars }: CalendarListProps) => {
|
||||
|
||||
if (calendarsIndex === -1) {
|
||||
console.error("Invalid calendar URL format - 'calendars' segment not found:", davCalendar.url);
|
||||
// Reset modal state to avoid stale data
|
||||
setSubscriptionModal({ isOpen: false, calendarName: "", caldavPath: null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,6 +81,8 @@ export const CalendarList = ({ calendars }: CalendarListProps) => {
|
||||
const remainingParts = pathParts.slice(calendarsIndex);
|
||||
if (remainingParts.length < 3) {
|
||||
console.error("Invalid calendar URL format - incomplete path:", davCalendar.url);
|
||||
// Reset modal state to avoid stale data
|
||||
setSubscriptionModal({ isOpen: false, calendarName: "", caldavPath: null });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,6 +96,8 @@ export const CalendarList = ({ calendars }: CalendarListProps) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to parse calendar URL:", error);
|
||||
// Reset modal state on error
|
||||
setSubscriptionModal({ isOpen: false, calendarName: "", caldavPath: null });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,8 +111,10 @@ export const CalendarList = ({ calendars }: CalendarListProps) => {
|
||||
// Use translation key for shared marker
|
||||
const sharedMarker = t('calendar.list.shared');
|
||||
|
||||
const sharedCalendars = calendarsArray.filter((cal) =>
|
||||
cal.name.includes(sharedMarker)
|
||||
// Memoize filtered calendars to avoid recalculation on every render
|
||||
const sharedCalendars = useMemo(
|
||||
() => calendarsArray.filter((cal) => cal.name.includes(sharedMarker)),
|
||||
[calendarsArray, sharedMarker]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -128,7 +128,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
// Now trigger a re-evaluation of eventFilter by calling refetchEvents
|
||||
calendarRef.current.refetchEvents();
|
||||
}
|
||||
}, [visibleCalendarUrls, davCalendars, calendarRef]);
|
||||
}, [visibleCalendarUrls, davCalendars]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -68,19 +68,11 @@ export const useSchedulerHandlers = ({
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[EventDrop] Event:', info.event);
|
||||
console.log('[EventDrop] allDay:', info.event.allDay);
|
||||
console.log('[EventDrop] start:', info.event.start);
|
||||
console.log('[EventDrop] end:', info.event.end);
|
||||
|
||||
try {
|
||||
const icsEvent = adapter.toIcsEvent(info.event as EventCalendarEvent, {
|
||||
defaultTimezone: extProps.timezone || BROWSER_TIMEZONE,
|
||||
});
|
||||
|
||||
console.log('[EventDrop] IcsEvent start:', icsEvent.start);
|
||||
console.log('[EventDrop] IcsEvent end:', icsEvent.end);
|
||||
|
||||
const result = await caldavService.updateEvent({
|
||||
eventUrl: extProps.eventUrl,
|
||||
event: icsEvent,
|
||||
|
||||
@@ -241,19 +241,23 @@ export const useSchedulerInit = ({
|
||||
calendarRef.current = ec as unknown as CalendarApi;
|
||||
|
||||
return () => {
|
||||
// @event-calendar/core is Svelte-based and uses $destroy
|
||||
// Always call $destroy before clearing the container to avoid memory leaks
|
||||
if (calendarRef.current) {
|
||||
// @event-calendar/core is Svelte-based and uses $destroy
|
||||
const calendar = calendarRef.current as CalendarApi;
|
||||
if (typeof calendar.$destroy === 'function') {
|
||||
calendar.$destroy();
|
||||
}
|
||||
calendarRef.current = null;
|
||||
}
|
||||
// Also clear the container
|
||||
// Clear the container only after calendar is destroyed
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = '';
|
||||
}
|
||||
};
|
||||
// Note: refs (containerRef, calendarRef, visibleCalendarUrlsRef, davCalendarsRef) are excluded
|
||||
// from dependencies as they are stable references that don't trigger re-renders
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isConnected,
|
||||
calendarUrl,
|
||||
@@ -270,15 +274,12 @@ export const useSchedulerInit = ({
|
||||
setCurrentDate,
|
||||
t,
|
||||
i18n.language,
|
||||
containerRef,
|
||||
calendarRef,
|
||||
visibleCalendarUrlsRef,
|
||||
davCalendarsRef,
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check scheduling capabilities on mount.
|
||||
* Silently verifies CalDAV scheduling support without debug output.
|
||||
*/
|
||||
export const useSchedulingCapabilitiesCheck = (
|
||||
isConnected: boolean,
|
||||
@@ -291,68 +292,8 @@ export const useSchedulingCapabilitiesCheck = (
|
||||
|
||||
hasCheckedRef.current = true;
|
||||
|
||||
const checkSchedulingCapabilities = async () => {
|
||||
const result = await caldavService.getSchedulingCapabilities();
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.group('📅 CalDAV Scheduling Capabilities');
|
||||
console.log(
|
||||
'Scheduling Support:',
|
||||
result.data.hasSchedulingSupport ? '✅ Enabled' : '❌ Disabled'
|
||||
);
|
||||
console.log(
|
||||
'Schedule Outbox URL:',
|
||||
result.data.scheduleOutboxUrl || '❌ Not found'
|
||||
);
|
||||
console.log(
|
||||
'Schedule Inbox URL:',
|
||||
result.data.scheduleInboxUrl || '❌ Not found'
|
||||
);
|
||||
console.log(
|
||||
'Calendar User Addresses:',
|
||||
result.data.calendarUserAddressSet.length > 0
|
||||
? result.data.calendarUserAddressSet
|
||||
: '❌ None'
|
||||
);
|
||||
console.log('');
|
||||
console.log('Raw server response:', result.data.rawResponse);
|
||||
|
||||
if (result.data.hasSchedulingSupport) {
|
||||
console.log('');
|
||||
console.log('✉️ Email Notifications Status:');
|
||||
console.log(' The server supports CalDAV scheduling (RFC 6638).');
|
||||
console.log(
|
||||
' However, this does NOT guarantee email notifications will be sent.'
|
||||
);
|
||||
console.log(
|
||||
' Email sending requires the IMip plugin to be configured on the server.'
|
||||
);
|
||||
console.log(
|
||||
' Contact your server administrator to verify IMip plugin configuration.'
|
||||
);
|
||||
} else {
|
||||
console.warn('');
|
||||
console.warn(
|
||||
'⚠️ CalDAV scheduling properties not found on this server.'
|
||||
);
|
||||
console.warn(' This could mean:');
|
||||
console.warn(
|
||||
' 1. The scheduling plugin is not enabled in Sabre/DAV configuration'
|
||||
);
|
||||
console.warn(
|
||||
' 2. The properties are located elsewhere (check raw response above)'
|
||||
);
|
||||
console.warn(
|
||||
' 3. The server does not support CalDAV scheduling (RFC 6638)'
|
||||
);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
} else {
|
||||
console.error('Failed to check scheduling capabilities:', result.error);
|
||||
}
|
||||
};
|
||||
|
||||
checkSchedulingCapabilities();
|
||||
// Silently check scheduling capabilities
|
||||
// Debug logging removed for production
|
||||
caldavService.getSchedulingCapabilities();
|
||||
}, [isConnected, caldavService]);
|
||||
};
|
||||
|
||||
@@ -172,6 +172,8 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
|
||||
// Connect to CalDAV server on mount
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
const result = await caldavService.connect({
|
||||
@@ -179,18 +181,37 @@ export const CalendarContextProvider = ({ children }: CalendarContextProviderPro
|
||||
headers,
|
||||
fetchOptions,
|
||||
});
|
||||
if (result.success) {
|
||||
if (isMounted && result.success) {
|
||||
setIsConnected(true);
|
||||
await refreshCalendars();
|
||||
} else {
|
||||
// Fetch calendars after successful connection
|
||||
const calendarsResult = await caldavService.fetchCalendars();
|
||||
if (isMounted && calendarsResult.success && calendarsResult.data) {
|
||||
setDavCalendars(calendarsResult.data);
|
||||
setVisibleCalendarUrls(new Set(calendarsResult.data.map(cal => cal.url)));
|
||||
}
|
||||
setIsLoading(false);
|
||||
} else if (isMounted) {
|
||||
console.error("Failed to connect to CalDAV:", result.error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error connecting to CalDAV:", error);
|
||||
if (isMounted) {
|
||||
console.error("Error connecting to CalDAV:", error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
connect();
|
||||
}, [caldavService, refreshCalendars]);
|
||||
|
||||
// Cleanup: prevent state updates after unmount
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
// 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 = {
|
||||
calendarRef,
|
||||
|
||||
@@ -864,8 +864,6 @@ export class CalDavService {
|
||||
? outboxUrl
|
||||
: `${this._account!.serverUrl}${outboxUrl.startsWith('/') ? outboxUrl.slice(1) : outboxUrl}`
|
||||
|
||||
console.log('[CalDAVService] Sending scheduling request to:', fullOutboxUrl)
|
||||
|
||||
// Use fetch directly to avoid davRequest URL construction issues in dev mode
|
||||
const response = await fetch(fullOutboxUrl, {
|
||||
method: 'POST',
|
||||
@@ -1208,8 +1206,6 @@ END:VCALENDAR`
|
||||
}
|
||||
|
||||
return withErrorHandling(async () => {
|
||||
console.log('[Scheduling Debug] Requesting from principal URL:', this._account!.principalUrl)
|
||||
|
||||
const response = await propfind({
|
||||
url: this._account!.principalUrl!,
|
||||
props: {
|
||||
@@ -1222,10 +1218,7 @@ END:VCALENDAR`
|
||||
depth: '0',
|
||||
})
|
||||
|
||||
console.log('[Scheduling Debug] Full PROPFIND response:', JSON.stringify(response, null, 2))
|
||||
|
||||
const props = response[0]?.props ?? {}
|
||||
console.log('[Scheduling Debug] Extracted props:', props)
|
||||
|
||||
// Note: tsdav converts XML property names to camelCase
|
||||
// schedule-outbox-URL becomes scheduleOutboxURL
|
||||
|
||||
@@ -327,7 +327,6 @@ export class EventCalendarAdapter {
|
||||
if (isAllDay && typeof dateValue === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateValue)) {
|
||||
const [year, month, day] = dateValue.split('-').map(Number)
|
||||
const result = new Date(Date.UTC(year, month - 1, day))
|
||||
console.log('[EventCalendarAdapter] Parsing all-day date:', dateValue, '→', result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -335,15 +334,12 @@ export class EventCalendarAdapter {
|
||||
}
|
||||
|
||||
const isAllDay = ecEvent.allDay ?? false
|
||||
console.log('[EventCalendarAdapter] toIcsEvent - allDay:', isAllDay, 'start:', ecEvent.start, 'end:', ecEvent.end)
|
||||
|
||||
const startDate = parseDate(ecEvent.start, isAllDay)
|
||||
const endDate = ecEvent.end
|
||||
? parseDate(ecEvent.end, isAllDay)
|
||||
: startDate
|
||||
|
||||
console.log('[EventCalendarAdapter] Parsed dates - start:', startDate, 'end:', endDate)
|
||||
|
||||
// Determine timezone
|
||||
const timezone = extProps.timezone ?? opts.defaultTimezone
|
||||
|
||||
|
||||
Reference in New Issue
Block a user