🔥(front) remove deprecated calendar components
Remove old CalendarView, CalendarList, EventModal and CreateCalendarModal replaced by new Scheduler and CalendarList implementations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* CalendarList component - List of calendars with visibility toggles.
|
||||
*/
|
||||
|
||||
import { Button, Checkbox } from "@openfun/cunningham-react";
|
||||
|
||||
import { Calendar } from "../api";
|
||||
import { useToggleCalendarVisibility } from "../hooks/useCalendars";
|
||||
|
||||
interface CalendarListProps {
|
||||
calendars: Calendar[];
|
||||
onCreateCalendar: () => void;
|
||||
}
|
||||
|
||||
export const CalendarList = ({
|
||||
calendars,
|
||||
onCreateCalendar,
|
||||
}: CalendarListProps) => {
|
||||
const toggleVisibility = useToggleCalendarVisibility();
|
||||
|
||||
// Ensure calendars is an array
|
||||
const calendarsArray = Array.isArray(calendars) ? calendars : [];
|
||||
|
||||
const ownedCalendars = calendarsArray.filter(
|
||||
(cal) => !cal.name.includes("(partagé)")
|
||||
);
|
||||
const sharedCalendars = calendarsArray.filter((cal) =>
|
||||
cal.name.includes("(partagé)")
|
||||
);
|
||||
|
||||
const handleToggle = (calendarId: string) => {
|
||||
toggleVisibility.mutate(calendarId);
|
||||
};
|
||||
|
||||
const renderCalendarItem = (calendar: Calendar) => (
|
||||
<div key={calendar.id} className="calendar-list__item">
|
||||
<Checkbox
|
||||
checked={calendar.is_visible}
|
||||
onChange={() => handleToggle(calendar.id)}
|
||||
label=""
|
||||
aria-label={`Afficher ${calendar.name}`}
|
||||
/>
|
||||
<span
|
||||
className="calendar-list__color"
|
||||
style={{ backgroundColor: calendar.color }}
|
||||
/>
|
||||
<span className="calendar-list__name" title={calendar.name}>
|
||||
{calendar.name}
|
||||
</span>
|
||||
{calendar.is_default && (
|
||||
<span className="calendar-list__badge">Par défaut</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="calendar-list">
|
||||
<div className="calendar-list__section">
|
||||
<div className="calendar-list__section-header">
|
||||
<span className="calendar-list__section-title">Mes calendriers</span>
|
||||
<button
|
||||
className="calendar-list__add-btn"
|
||||
onClick={onCreateCalendar}
|
||||
title="Créer un calendrier"
|
||||
>
|
||||
<span className="material-icons">add</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="calendar-list__items">
|
||||
{ownedCalendars.map(renderCalendarItem)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sharedCalendars.length > 0 && (
|
||||
<div className="calendar-list__section">
|
||||
<div className="calendar-list__section-header">
|
||||
<span className="calendar-list__section-title">
|
||||
Calendriers partagés
|
||||
</span>
|
||||
</div>
|
||||
<div className="calendar-list__items">
|
||||
{sharedCalendars.map(renderCalendarItem)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,171 +0,0 @@
|
||||
.calendar-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
|
||||
&__loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 2rem;
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&--loading,
|
||||
&--error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--c--theme--colors--primary-500);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--c--theme--colors--primary-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open-calendar overrides to match La Suite theme
|
||||
.calendar-view__container {
|
||||
// Calendar toolbar
|
||||
.ec-toolbar {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
}
|
||||
|
||||
// Today button
|
||||
.ec-button {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--c--theme--colors--greyscale-300);
|
||||
background: var(--c--theme--colors--greyscale-000);
|
||||
color: var(--c--theme--colors--greyscale-800);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
&.ec-active {
|
||||
background: var(--c--theme--colors--primary-500);
|
||||
border-color: var(--c--theme--colors--primary-500);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
.ec-prev,
|
||||
.ec-next {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// Title
|
||||
.ec-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--c--theme--colors--greyscale-900);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
// Days header
|
||||
.ec-days,
|
||||
.ec-day {
|
||||
border-color: var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
|
||||
.ec-day-head {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
}
|
||||
|
||||
// Time slots
|
||||
.ec-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
}
|
||||
|
||||
// Events
|
||||
.ec-event {
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 4px;
|
||||
border: none;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Today highlight
|
||||
.ec-today {
|
||||
background: rgba(var(--c--theme--colors--primary-rgb, 49, 116, 173), 0.1);
|
||||
}
|
||||
|
||||
// Now indicator
|
||||
.ec-now-indicator {
|
||||
border-color: var(--c--theme--colors--danger-500);
|
||||
|
||||
&::before {
|
||||
background: var(--c--theme--colors--danger-500);
|
||||
}
|
||||
}
|
||||
|
||||
// Popup styles
|
||||
.ec-popup {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid var(--c--theme--colors--greyscale-200);
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
/**
|
||||
* CalendarView component using open-calendar (Algoo).
|
||||
* Renders a CalDAV-connected calendar view.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useAuth } from "@/features/auth/Auth";
|
||||
import { createEventModalHandlers, type ModalState } from "./EventModalAdapter";
|
||||
import { useEventModal } from "../hooks/useEventModal";
|
||||
import type { IcsEvent } from 'ts-ics';
|
||||
|
||||
interface CalendarViewProps {
|
||||
selectedDate?: Date;
|
||||
onSelectDate?: (date: Date) => void;
|
||||
}
|
||||
|
||||
export const CalendarView = ({
|
||||
selectedDate = new Date(),
|
||||
onSelectDate,
|
||||
}: CalendarViewProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const calendarRef = useRef<unknown>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [modalState, setModalState] = useState<ModalState>({
|
||||
isOpen: false,
|
||||
mode: 'create',
|
||||
event: null,
|
||||
calendarUrl: '',
|
||||
calendars: [],
|
||||
handleSave: null,
|
||||
handleDelete: null,
|
||||
});
|
||||
const { user } = useAuth();
|
||||
|
||||
// Use the modal hook with state from adapter
|
||||
const modal = useEventModal({
|
||||
calendars: modalState.calendars,
|
||||
initialEvent: modalState.event,
|
||||
initialCalendarUrl: modalState.calendarUrl,
|
||||
onSubmit: async (event: IcsEvent, calendarUrl: string) => {
|
||||
if (modalState.handleSave) {
|
||||
await modalState.handleSave({ calendarUrl, event });
|
||||
setModalState(prev => ({ ...prev, isOpen: false }));
|
||||
}
|
||||
},
|
||||
onDelete: async (event: IcsEvent, calendarUrl: string) => {
|
||||
if (modalState.handleDelete) {
|
||||
await modalState.handleDelete({ calendarUrl, event });
|
||||
setModalState(prev => ({ ...prev, isOpen: false }));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Sync modal state with adapter state
|
||||
useEffect(() => {
|
||||
if (modalState.isOpen) {
|
||||
// Always call open to update all state (event, mode, calendarUrl, calendars)
|
||||
modal.open(modalState.event, modalState.mode, modalState.calendarUrl, modalState.calendars);
|
||||
} else if (modal.isOpen) {
|
||||
modal.close();
|
||||
}
|
||||
}, [modalState.isOpen, modalState.event, modalState.mode, modalState.calendarUrl, modalState.calendars]);
|
||||
|
||||
useEffect(() => {
|
||||
const initCalendar = async () => {
|
||||
if (!containerRef.current || !user) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Dynamically import open-calendar to avoid SSR issues (uses browser-only globals)
|
||||
const { createCalendar } = await import("open-dav-calendar");
|
||||
|
||||
// Clear previous calendar instance
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = "";
|
||||
}
|
||||
|
||||
// CalDAV server URL - proxied through Django backend
|
||||
// The proxy handles authentication via session cookies
|
||||
// open-calendar will discover calendars from this URL
|
||||
const caldavServerUrl = `${process.env.NEXT_PUBLIC_API_ORIGIN}/api/v1.0/caldav/`;
|
||||
|
||||
// Create calendar with CalDAV source
|
||||
// Use fetchOptions with credentials to include cookies
|
||||
const calendar = await createCalendar(
|
||||
[
|
||||
{
|
||||
serverUrl: caldavServerUrl,
|
||||
fetchOptions: {
|
||||
credentials: "include" as RequestCredentials,
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[], // No address books for now
|
||||
containerRef.current,
|
||||
{
|
||||
view: "timeGridWeek",
|
||||
views: ["dayGridMonth", "timeGridWeek", "timeGridDay", "listWeek"],
|
||||
locale: "fr",
|
||||
date: selectedDate,
|
||||
editable: true,
|
||||
// Use custom EventEditHandlers that update React state
|
||||
...createEventModalHandlers(setModalState, []),
|
||||
onEventCreated: (info) => {
|
||||
console.log("Event created:", info);
|
||||
},
|
||||
onEventUpdated: (info) => {
|
||||
console.log("Event updated:", info);
|
||||
},
|
||||
onEventDeleted: (info) => {
|
||||
console.log("Event deleted:", info);
|
||||
},
|
||||
},
|
||||
{
|
||||
// French translations
|
||||
calendar: {
|
||||
today: "Aujourd'hui",
|
||||
month: "Mois",
|
||||
week: "Semaine",
|
||||
day: "Jour",
|
||||
list: "Liste",
|
||||
},
|
||||
event: {
|
||||
edit: "Modifier",
|
||||
delete: "Supprimer",
|
||||
save: "Enregistrer",
|
||||
cancel: "Annuler",
|
||||
title: "Titre",
|
||||
description: "Description",
|
||||
location: "Lieu",
|
||||
startDate: "Date de début",
|
||||
endDate: "Date de fin",
|
||||
allDay: "Journée entière",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
calendarRef.current = calendar;
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to initialize calendar:", err);
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Erreur lors du chargement du calendrier"
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initCalendar();
|
||||
|
||||
return () => {
|
||||
// Cleanup
|
||||
if (containerRef.current) {
|
||||
containerRef.current.innerHTML = "";
|
||||
}
|
||||
calendarRef.current = null;
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
// Update calendar date when selectedDate changes
|
||||
useEffect(() => {
|
||||
if (calendarRef.current && selectedDate) {
|
||||
// open-calendar may have a method to set date
|
||||
// This depends on the CalendarElement API
|
||||
}
|
||||
}, [selectedDate]);
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="calendar-view calendar-view--loading">
|
||||
<p>Connexion requise pour afficher le calendrier</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="calendar-view calendar-view--error">
|
||||
<p>{error}</p>
|
||||
<button onClick={() => window.location.reload()}>Réessayer</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="calendar-view">
|
||||
{isLoading && (
|
||||
<div className="calendar-view__loading">
|
||||
<p>Chargement du calendrier...</p>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="calendar-view__container"
|
||||
style={{ opacity: isLoading ? 0 : 1 }}
|
||||
/>
|
||||
{modal.Modal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
/**
|
||||
* Modal component for creating a new calendar.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalSize,
|
||||
useModal,
|
||||
} from "@openfun/cunningham-react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useCreateCalendar } from "../hooks/useCalendars";
|
||||
import { addToast, ToasterItem } from "@/features/ui/components/toaster/Toaster";
|
||||
import { errorToString } from "@/features/api/APIError";
|
||||
|
||||
export const useCreateCalendarModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const modal = useModal();
|
||||
const createCalendar = useCreateCalendar();
|
||||
const [name, setName] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Reset form when modal opens
|
||||
useEffect(() => {
|
||||
if (modal.isOpen) {
|
||||
setName("");
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [modal.isOpen]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await createCalendar.mutateAsync({
|
||||
name: name.trim(),
|
||||
});
|
||||
addToast(
|
||||
<ToasterItem>
|
||||
<span>{t("calendar.created_success", { name: name.trim(), defaultValue: `Calendrier "${name.trim()}" créé avec succès` })}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
setName("");
|
||||
modal.close();
|
||||
} catch (error) {
|
||||
console.error("Failed to create calendar:", error);
|
||||
const errorMessage = errorToString(error);
|
||||
addToast(
|
||||
<ToasterItem type="error">
|
||||
<span>{errorMessage || t("calendar.created_error", { defaultValue: "Erreur lors de la création du calendrier" })}</span>
|
||||
</ToasterItem>
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setName("");
|
||||
modal.close();
|
||||
};
|
||||
|
||||
return {
|
||||
...modal,
|
||||
Modal: (
|
||||
<Modal
|
||||
{...modal}
|
||||
title={t("calendar.create_modal.title", { defaultValue: "Créer un nouveau calendrier" })}
|
||||
size={ModalSize.SMALL}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
label={t("calendar.create_modal.name_label", { defaultValue: "Nom du calendrier" })}
|
||||
value={name}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
placeholder={t("calendar.create_modal.name_placeholder", { defaultValue: "Mon calendrier" })}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: "1rem", justifyContent: "flex-end", marginTop: "1.5rem" }}>
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("common.cancel", { defaultValue: "Annuler" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={!name.trim() || isSubmitting}
|
||||
>
|
||||
{isSubmitting
|
||||
? t("common.creating", { defaultValue: "Création..." })
|
||||
: t("common.create", { defaultValue: "Créer" })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,441 +0,0 @@
|
||||
import { Modal, Button, Input, TextArea, Select } from '@openfun/cunningham-react';
|
||||
import type { IcsEvent, IcsRecurrenceRule } from 'ts-ics';
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { RecurrenceEditor } from './RecurrenceEditor';
|
||||
|
||||
// Helper function to check if event is all-day (same logic as open-dav-calendar)
|
||||
function isEventAllDay(event: IcsEvent): boolean {
|
||||
return event.start.type === 'DATE' || (event.end?.type === 'DATE');
|
||||
}
|
||||
|
||||
// Calendar type from open-calendar (exported for use in other components)
|
||||
export interface Calendar {
|
||||
url: string;
|
||||
uid?: unknown;
|
||||
displayName?: string;
|
||||
calendarColor?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface EventModalProps {
|
||||
isOpen: boolean;
|
||||
mode: 'create' | 'edit';
|
||||
calendars: Calendar[];
|
||||
selectedEvent?: IcsEvent | null;
|
||||
calendarUrl: string;
|
||||
onSubmit: (event: IcsEvent, calendarUrl: string) => Promise<void>;
|
||||
onAllDayChange: (isAllDay: boolean) => void;
|
||||
onClose: () => void;
|
||||
onDelete?: (event: IcsEvent, calendarUrl: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const VISIBILITY_OPTIONS = [
|
||||
{ value: 'default', label: 'Par défaut' },
|
||||
{ value: 'public', label: 'Public' },
|
||||
{ value: 'private', label: 'Privé' },
|
||||
] as const;
|
||||
|
||||
export function EventModal({
|
||||
isOpen,
|
||||
mode,
|
||||
calendars,
|
||||
selectedEvent,
|
||||
calendarUrl,
|
||||
onSubmit,
|
||||
onAllDayChange,
|
||||
onClose,
|
||||
onDelete,
|
||||
}: EventModalProps) {
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
const [formData, setFormData] = useState<Partial<IcsEvent & { startTime?: string; endTime?: string }>>({});
|
||||
const [activeFeatures, setActiveFeatures] = useState<Set<string>>(new Set());
|
||||
const [selectedCalendarUrl, setSelectedCalendarUrl] = useState<string>(calendarUrl);
|
||||
const [allDay, setAllDay] = useState<boolean>(false);
|
||||
|
||||
// Helper to get local date from IcsDateObject
|
||||
const getLocalDate = (dateObj: { date: Date; local?: { date: Date; timezone: string } }): Date => {
|
||||
return dateObj.local?.date || dateObj.date;
|
||||
};
|
||||
|
||||
// Format date for input
|
||||
const formatDateForInput = (date: Date): string => {
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// Format time for input
|
||||
const formatTimeForInput = (date: Date): string => {
|
||||
return date.toTimeString().slice(0, 5);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEvent) {
|
||||
const eventAllDay = isEventAllDay(selectedEvent);
|
||||
const startDate = getLocalDate(selectedEvent.start);
|
||||
const endDate = selectedEvent.end ? getLocalDate(selectedEvent.end) : startDate;
|
||||
|
||||
setAllDay(eventAllDay);
|
||||
setFormData({
|
||||
...selectedEvent,
|
||||
summary: selectedEvent.summary || '',
|
||||
startTime: eventAllDay ? undefined : formatTimeForInput(startDate),
|
||||
endTime: eventAllDay ? undefined : formatTimeForInput(endDate),
|
||||
});
|
||||
setSelectedCalendarUrl(calendarUrl);
|
||||
} else {
|
||||
const now = new Date();
|
||||
const endTime = new Date(now.getTime() + 30 * 60 * 1000);
|
||||
setAllDay(false);
|
||||
setFormData({
|
||||
summary: '',
|
||||
start: {
|
||||
type: 'DATE-TIME',
|
||||
date: now,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
end: {
|
||||
type: 'DATE-TIME',
|
||||
date: endTime,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
startTime: formatTimeForInput(now),
|
||||
endTime: formatTimeForInput(endTime),
|
||||
});
|
||||
setSelectedCalendarUrl(calendars[0]?.url || '');
|
||||
}
|
||||
}, [selectedEvent, calendars, calendarUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
titleInputRef.current?.focus();
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const startDate = formData.start ? getLocalDate(formData.start) : new Date();
|
||||
const endDate = formData.end ? getLocalDate(formData.end) : startDate;
|
||||
|
||||
const toggleFeature = (feature: string) => {
|
||||
if (feature === 'allDay') {
|
||||
const newAllDay = !allDay;
|
||||
setAllDay(newAllDay);
|
||||
onAllDayChange(newAllDay);
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveFeatures(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(feature)) {
|
||||
next.delete(feature);
|
||||
} else {
|
||||
next.add(feature);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.summary) return;
|
||||
|
||||
const startDateValue = new Date(`${formatDateForInput(startDate)}T${allDay ? '00:00:00' : formData.startTime || '00:00:00'}`);
|
||||
const endDateValue = new Date(`${formatDateForInput(endDate)}T${allDay ? '00:00:00' : formData.endTime || '00:00:00'}`);
|
||||
|
||||
const updatedEvent: IcsEvent = {
|
||||
...(selectedEvent || {}),
|
||||
uid: selectedEvent?.uid || `event-${Date.now()}`,
|
||||
summary: formData.summary || '',
|
||||
location: formData.location || undefined,
|
||||
description: formData.description || formData.notes || undefined,
|
||||
start: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: startDateValue,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
end: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: endDateValue,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
recurrenceRule: formData.recurrenceRule,
|
||||
};
|
||||
|
||||
await onSubmit(updatedEvent, selectedCalendarUrl);
|
||||
};
|
||||
|
||||
const renderFeatureContent = () => {
|
||||
return (
|
||||
<>
|
||||
{activeFeatures.has('calendar') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<Select
|
||||
label="Calendrier"
|
||||
name="calendar-select"
|
||||
value={selectedCalendarUrl}
|
||||
onChange={(e) => setSelectedCalendarUrl(e.target.value)}
|
||||
options={calendars.map(cal => ({
|
||||
value: cal.url,
|
||||
label: cal.displayName || cal.url
|
||||
}))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFeatures.has('repeat') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<RecurrenceEditor
|
||||
value={formData.recurrenceRule}
|
||||
onChange={(recurrenceRule) => {
|
||||
setFormData(prev => ({ ...prev, recurrenceRule }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFeatures.has('location') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<Input
|
||||
label="Lieu"
|
||||
placeholder="Ajouter un lieu"
|
||||
value={formData.location || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, location: e.target.value }))}
|
||||
icon={<span className="material-icons">location_on</span>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeFeatures.has('notes') && (
|
||||
<div className="event-modal-layout event-modal-layout--margin-bottom-1rem">
|
||||
<TextArea
|
||||
label="Notes"
|
||||
placeholder="Ajouter des notes"
|
||||
value={formData.description || formData.notes || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value, notes: e.target.value }))}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const selectedCalendar = calendars.find(cal => cal.url === selectedCalendarUrl);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={mode === 'create' ? 'Créer un événement' : 'Modifier l\'événement'}
|
||||
>
|
||||
<div className="event-modal-layout event-modal-layout--gap-2rem">
|
||||
<Input
|
||||
label="Titre"
|
||||
value={formData.summary || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, summary: e.target.value }))}
|
||||
ref={titleInputRef}
|
||||
/>
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-1rem">
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="date"
|
||||
label="Date de début"
|
||||
name="event-start-date"
|
||||
value={formatDateForInput(startDate)}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
const currentTime = allDay ? '00:00:00' : (formData.startTime || formatTimeForInput(startDate));
|
||||
const [hours, minutes] = currentTime.split(':');
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
start: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
startTime: allDay ? undefined : currentTime,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{!allDay && (
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
label="Heure de début"
|
||||
name="event-start-time"
|
||||
value={formData.startTime || formatTimeForInput(startDate)}
|
||||
onChange={(e) => {
|
||||
const [hours, minutes] = e.target.value.split(':');
|
||||
const newDate = new Date(startDate);
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
start: {
|
||||
type: 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
startTime: e.target.value,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-1rem">
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="date"
|
||||
label="Date de fin"
|
||||
name="event-end-date"
|
||||
value={formatDateForInput(endDate)}
|
||||
onChange={(e) => {
|
||||
const newDate = new Date(e.target.value);
|
||||
const currentTime = allDay ? '00:00:00' : (formData.endTime || formatTimeForInput(endDate));
|
||||
const [hours, minutes] = currentTime.split(':');
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
end: {
|
||||
type: allDay ? 'DATE' : 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
endTime: allDay ? undefined : currentTime,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{!allDay && (
|
||||
<div className="event-modal-layout event-modal-layout--flex-1">
|
||||
<Input
|
||||
type="time"
|
||||
label="Heure de fin"
|
||||
name="event-end-time"
|
||||
value={formData.endTime || formatTimeForInput(endDate)}
|
||||
onChange={(e) => {
|
||||
const [hours, minutes] = e.target.value.split(':');
|
||||
const newDate = new Date(endDate);
|
||||
newDate.setHours(parseInt(hours || '0'), parseInt(minutes || '0'));
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
end: {
|
||||
type: 'DATE-TIME',
|
||||
date: newDate,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
},
|
||||
endTime: e.target.value,
|
||||
}));
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{renderFeatureContent()}
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-0-5rem event-modal-layout--wrap">
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('calendar') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('calendar')}
|
||||
style={selectedCalendar?.calendarColor ? {
|
||||
backgroundColor: selectedCalendar.calendarColor,
|
||||
color: 'white',
|
||||
} : undefined}
|
||||
>
|
||||
<span className="material-icons">event_note</span>
|
||||
{selectedCalendar?.displayName || selectedCalendar?.url || 'Calendrier'}
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('visibility') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('visibility')}
|
||||
>
|
||||
<span className="material-icons">visibility</span>
|
||||
Visibilité
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('repeat') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('repeat')}
|
||||
>
|
||||
<span className="material-icons">repeat</span>
|
||||
Répéter
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${allDay ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('allDay')}
|
||||
>
|
||||
<span className="material-icons">today</span>
|
||||
Toute la journée
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('invites') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('invites')}
|
||||
>
|
||||
<span className="material-icons">people</span>
|
||||
Invités
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('location') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('location')}
|
||||
>
|
||||
<span className="material-icons">location_on</span>
|
||||
Lieu
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('visio') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('visio')}
|
||||
>
|
||||
<span className="material-icons">videocam</span>
|
||||
Visio
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('notification') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('notification')}
|
||||
>
|
||||
<span className="material-icons">notifications</span>
|
||||
Rappel
|
||||
</button>
|
||||
<button
|
||||
className={`event-modal__feature-tag ${activeFeatures.has('notes') ? 'event-modal__feature-tag--active' : ''}`}
|
||||
onClick={() => toggleFeature('notes')}
|
||||
>
|
||||
<span className="material-icons">notes</span>
|
||||
Notes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--justify-space-between event-modal-layout--margin-top-2rem">
|
||||
{mode === 'edit' && selectedEvent && (
|
||||
<Button
|
||||
type="button"
|
||||
color="danger"
|
||||
onClick={() => onDelete?.(selectedEvent, selectedCalendarUrl)}
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
<div className="event-modal-layout event-modal-layout--row event-modal-layout--gap-1rem event-modal-layout--margin-left-auto">
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
{mode === 'create' ? 'Créer' : 'Enregistrer'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* Adapter that bridges open-calendar's EventEditHandlers with React state.
|
||||
* The handlers update state, and the modal is rendered normally in React tree.
|
||||
*/
|
||||
|
||||
import type { IcsEvent } from 'ts-ics';
|
||||
import type { OpenCalendar } from '../hooks/useEventModal';
|
||||
|
||||
interface EventEditHandlers {
|
||||
onCreateEvent: (info: EventEditCreateInfo) => void;
|
||||
onSelectEvent: (info: EventEditSelectInfo) => void;
|
||||
onMoveResizeEvent: (info: EventEditMoveResizeInfo) => void;
|
||||
onDeleteEvent: (info: EventEditDeleteInfo) => void;
|
||||
}
|
||||
|
||||
interface EventEditCreateInfo {
|
||||
jsEvent: Event;
|
||||
userContact?: any;
|
||||
event: IcsEvent;
|
||||
calendars: OpenCalendar[];
|
||||
vCards: any[];
|
||||
handleCreate: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
interface EventEditSelectInfo {
|
||||
jsEvent: Event;
|
||||
userContact?: any;
|
||||
calendarUrl: string;
|
||||
event: IcsEvent;
|
||||
recurringEvent?: IcsEvent;
|
||||
calendars: OpenCalendar[];
|
||||
vCards: any[];
|
||||
handleUpdate: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
handleDelete: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
interface EventEditMoveResizeInfo {
|
||||
jsEvent: Event;
|
||||
calendarUrl: string;
|
||||
userContact?: any;
|
||||
event: IcsEvent;
|
||||
recurringEvent?: IcsEvent;
|
||||
start: Date;
|
||||
end: Date;
|
||||
handleUpdate: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
interface EventEditDeleteInfo {
|
||||
jsEvent: Event;
|
||||
calendarUrl: string;
|
||||
userContact?: any;
|
||||
event: IcsEvent;
|
||||
recurringEvent?: IcsEvent;
|
||||
handleDelete: (event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>;
|
||||
}
|
||||
|
||||
export interface ModalState {
|
||||
isOpen: boolean;
|
||||
mode: 'create' | 'edit';
|
||||
event: IcsEvent | null;
|
||||
calendarUrl: string;
|
||||
calendars: OpenCalendar[];
|
||||
handleSave: ((event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>) | null;
|
||||
handleDelete: ((event: { calendarUrl: string; event: IcsEvent }) => Promise<Response>) | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates EventEditHandlers that update the provided state setter.
|
||||
*/
|
||||
export function createEventModalHandlers(
|
||||
setState: (state: ModalState) => void,
|
||||
calendars: OpenCalendar[] | (() => OpenCalendar[])
|
||||
): EventEditHandlers {
|
||||
const getCalendars = (): OpenCalendar[] => {
|
||||
return typeof calendars === 'function' ? calendars() : calendars;
|
||||
};
|
||||
|
||||
return {
|
||||
onCreateEvent: ({ event, calendars: calList, handleCreate }: EventEditCreateInfo) => {
|
||||
setState({
|
||||
isOpen: true,
|
||||
mode: 'create',
|
||||
event,
|
||||
calendarUrl: calList[0]?.url || '',
|
||||
calendars: calList,
|
||||
handleSave: handleCreate,
|
||||
handleDelete: null,
|
||||
});
|
||||
},
|
||||
onSelectEvent: ({ calendarUrl, event, calendars: calList, handleUpdate, handleDelete }: EventEditSelectInfo) => {
|
||||
setState({
|
||||
isOpen: true,
|
||||
mode: 'edit',
|
||||
event,
|
||||
calendarUrl,
|
||||
calendars: calList,
|
||||
handleSave: handleUpdate,
|
||||
handleDelete,
|
||||
});
|
||||
},
|
||||
onMoveResizeEvent: ({ calendarUrl, event, start, end, handleUpdate }: EventEditMoveResizeInfo) => {
|
||||
const newEvent = { ...event };
|
||||
const startDelta = start.getTime() - event.start.date.getTime();
|
||||
newEvent.start = { ...newEvent.start, date: new Date(event.start.date.getTime() + startDelta) };
|
||||
if (event.end) {
|
||||
const endDelta = end.getTime() - event.end.date.getTime();
|
||||
newEvent.end = { ...newEvent.end, date: new Date(event.end.date.getTime() + endDelta) };
|
||||
}
|
||||
handleUpdate({ calendarUrl, event: newEvent });
|
||||
},
|
||||
onDeleteEvent: ({ calendarUrl, event, handleDelete }: EventEditDeleteInfo) => {
|
||||
handleDelete({ calendarUrl, event });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user