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