♻️(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:
Nathan Panchout
2026-01-25 21:00:55 +01:00
parent aa7939b527
commit 32e00a2210
9 changed files with 75 additions and 106 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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 (
<>

View File

@@ -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,

View File

@@ -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]);
};

View File

@@ -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,

View File

@@ -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

View File

@@ -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