✨(front) add Scheduler modals
Add EventModal for creating and editing events with support for recurrence, attendees and all-day events. Add DeleteEventModal with options for recurring event deletion scope. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* DeleteEventModal component.
|
||||
* Displays options for deleting recurring events or confirms single event deletion.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Modal, ModalSize } from "@gouvfr-lasuite/cunningham-react";
|
||||
|
||||
import type { DeleteEventModalProps, RecurringDeleteOption } from "./types";
|
||||
|
||||
export const DeleteEventModal = ({
|
||||
isOpen,
|
||||
isRecurring,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: DeleteEventModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedOption, setSelectedOption] =
|
||||
useState<RecurringDeleteOption>('this');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onCancel}
|
||||
title={t('calendar.event.deleteConfirm')}
|
||||
size={ModalSize.SMALL}
|
||||
rightActions={
|
||||
<>
|
||||
<Button color="neutral" onClick={onCancel}>
|
||||
{t('calendar.event.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
onClick={() => onConfirm(isRecurring ? selectedOption : undefined)}
|
||||
>
|
||||
{t('calendar.event.delete')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="delete-modal__content">
|
||||
{isRecurring ? (
|
||||
<>
|
||||
<p className="delete-modal__message">
|
||||
{t('calendar.event.deleteRecurringPrompt')}
|
||||
</p>
|
||||
<div className="delete-modal__options">
|
||||
<label className="delete-modal__option">
|
||||
<input
|
||||
type="radio"
|
||||
name="delete-option"
|
||||
value="this"
|
||||
checked={selectedOption === 'this'}
|
||||
onChange={(e) =>
|
||||
setSelectedOption(e.target.value as RecurringDeleteOption)
|
||||
}
|
||||
/>
|
||||
<span>{t('calendar.event.deleteThisOccurrence')}</span>
|
||||
</label>
|
||||
<label className="delete-modal__option">
|
||||
<input
|
||||
type="radio"
|
||||
name="delete-option"
|
||||
value="future"
|
||||
checked={selectedOption === 'future'}
|
||||
onChange={(e) =>
|
||||
setSelectedOption(e.target.value as RecurringDeleteOption)
|
||||
}
|
||||
/>
|
||||
<span>{t('calendar.event.deleteThisAndFuture')}</span>
|
||||
</label>
|
||||
<label className="delete-modal__option">
|
||||
<input
|
||||
type="radio"
|
||||
name="delete-option"
|
||||
value="all"
|
||||
checked={selectedOption === 'all'}
|
||||
onChange={(e) =>
|
||||
setSelectedOption(e.target.value as RecurringDeleteOption)
|
||||
}
|
||||
/>
|
||||
<span>{t('calendar.event.deleteAllOccurrences')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="delete-modal__message">
|
||||
{t('calendar.event.deleteConfirmMessage')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,620 @@
|
||||
/**
|
||||
* EventModal component.
|
||||
* Handles creation and editing of calendar events.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
IcsEvent,
|
||||
IcsAttendee,
|
||||
IcsOrganizer,
|
||||
IcsRecurrenceRule,
|
||||
} from "ts-ics";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalSize,
|
||||
Select,
|
||||
TextArea,
|
||||
} from "@gouvfr-lasuite/cunningham-react";
|
||||
|
||||
import { useAuth } from "@/features/auth/Auth";
|
||||
import { AttendeesInput } from "../AttendeesInput";
|
||||
import { RecurrenceEditor } from "../RecurrenceEditor";
|
||||
import { DeleteEventModal } from "./DeleteEventModal";
|
||||
import type { EventModalProps, RecurringDeleteOption } from "./types";
|
||||
import {
|
||||
formatDateTimeLocal,
|
||||
formatDateLocal,
|
||||
parseDateTimeLocal,
|
||||
parseDateLocal,
|
||||
} from "./utils/dateFormatters";
|
||||
|
||||
// Get browser timezone
|
||||
const BROWSER_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
export const EventModal = ({
|
||||
isOpen,
|
||||
mode,
|
||||
event,
|
||||
calendarUrl,
|
||||
calendars,
|
||||
adapter,
|
||||
onSave,
|
||||
onDelete,
|
||||
onRespondToInvitation,
|
||||
onClose,
|
||||
}: EventModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const [title, setTitle] = useState(event?.summary || "");
|
||||
const [description, setDescription] = useState(event?.description || "");
|
||||
const [location, setLocation] = useState(event?.location || "");
|
||||
const [startDateTime, setStartDateTime] = useState("");
|
||||
const [endDateTime, setEndDateTime] = useState("");
|
||||
const [selectedCalendarUrl, setSelectedCalendarUrl] = useState(calendarUrl);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [attendees, setAttendees] = useState<IcsAttendee[]>([]);
|
||||
const [showAttendees, setShowAttendees] = useState(false);
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>(
|
||||
event?.recurrenceRule
|
||||
);
|
||||
const [showRecurrence, setShowRecurrence] = useState(() => {
|
||||
return !!event?.recurrenceRule;
|
||||
});
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [isAllDay, setIsAllDay] = useState(() => {
|
||||
return event?.start?.type === 'DATE';
|
||||
});
|
||||
|
||||
// Calculate organizer
|
||||
const organizer: IcsOrganizer | undefined =
|
||||
event?.organizer ||
|
||||
(user?.email
|
||||
? { email: user.email, name: user.email.split('@')[0] }
|
||||
: undefined);
|
||||
|
||||
// Check if current user is an attendee (invited to this event)
|
||||
const currentUserAttendee = event?.attendees?.find(
|
||||
(att) => user?.email && att.email.toLowerCase() === user.email.toLowerCase()
|
||||
);
|
||||
const isInvited = !!(
|
||||
event?.organizer &&
|
||||
currentUserAttendee &&
|
||||
event.organizer.email !== user?.email
|
||||
);
|
||||
const currentParticipationStatus =
|
||||
currentUserAttendee?.partstat || 'NEEDS-ACTION';
|
||||
|
||||
// Reset form when event changes
|
||||
useEffect(() => {
|
||||
setTitle(event?.summary || "");
|
||||
setDescription(event?.description || "");
|
||||
setLocation(event?.location || "");
|
||||
setSelectedCalendarUrl(calendarUrl);
|
||||
|
||||
// Initialize attendees from event
|
||||
if (event?.attendees && event.attendees.length > 0) {
|
||||
setAttendees(event.attendees);
|
||||
setShowAttendees(true);
|
||||
} else {
|
||||
setAttendees([]);
|
||||
setShowAttendees(false);
|
||||
}
|
||||
|
||||
// Initialize recurrence from event
|
||||
if (event?.recurrenceRule) {
|
||||
setRecurrence(event.recurrenceRule);
|
||||
setShowRecurrence(true);
|
||||
} else {
|
||||
setRecurrence(undefined);
|
||||
setShowRecurrence(false);
|
||||
}
|
||||
|
||||
// Initialize all-day from event
|
||||
const eventIsAllDay = event?.start?.type === 'DATE';
|
||||
setIsAllDay(eventIsAllDay);
|
||||
|
||||
// Parse start/end dates
|
||||
// Dates from adapter have timezone info and are "fake UTC" - use getUTC* methods
|
||||
if (event?.start?.date) {
|
||||
const startDate =
|
||||
event.start.date instanceof Date
|
||||
? event.start.date
|
||||
: new Date(event.start.date);
|
||||
// If there's timezone info, the date is "fake UTC"
|
||||
const isFakeUtc = Boolean(event.start.local?.timezone);
|
||||
|
||||
if (eventIsAllDay) {
|
||||
setStartDateTime(formatDateLocal(startDate, isFakeUtc));
|
||||
} else {
|
||||
setStartDateTime(formatDateTimeLocal(startDate, isFakeUtc));
|
||||
}
|
||||
} else {
|
||||
if (eventIsAllDay) {
|
||||
setStartDateTime(formatDateLocal(new Date()));
|
||||
} else {
|
||||
setStartDateTime(formatDateTimeLocal(new Date()));
|
||||
}
|
||||
}
|
||||
|
||||
if (event?.end?.date) {
|
||||
const endDate =
|
||||
event.end.date instanceof Date
|
||||
? event.end.date
|
||||
: new Date(event.end.date);
|
||||
const isFakeUtc = Boolean(event.end.local?.timezone);
|
||||
|
||||
if (eventIsAllDay) {
|
||||
// For all-day events, the end date in ICS is exclusive (next day)
|
||||
// But in the UI we want to show the inclusive end date (same day for 1-day event)
|
||||
// So subtract 1 day when displaying
|
||||
const displayEndDate = new Date(endDate);
|
||||
displayEndDate.setUTCDate(displayEndDate.getUTCDate() - 1);
|
||||
setEndDateTime(formatDateLocal(displayEndDate, isFakeUtc));
|
||||
} else {
|
||||
setEndDateTime(formatDateTimeLocal(endDate, isFakeUtc));
|
||||
}
|
||||
} else {
|
||||
if (eventIsAllDay) {
|
||||
// Default: same day
|
||||
setEndDateTime(formatDateLocal(new Date()));
|
||||
} else {
|
||||
// Default: 1 hour after start
|
||||
const defaultEnd = new Date();
|
||||
defaultEnd.setHours(defaultEnd.getHours() + 1);
|
||||
setEndDateTime(formatDateTimeLocal(defaultEnd));
|
||||
}
|
||||
}
|
||||
}, [event, calendarUrl]);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Remove duration to avoid union type conflict
|
||||
// (IcsEvent is either end or duration, not both)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { duration: _duration, ...eventWithoutDuration } = event ?? {};
|
||||
|
||||
let icsEvent: IcsEvent;
|
||||
|
||||
if (isAllDay) {
|
||||
// All-day event
|
||||
const startDate = parseDateLocal(startDateTime);
|
||||
const endDate = parseDateLocal(endDateTime);
|
||||
|
||||
// Create UTC dates for all-day events
|
||||
const utcStart = new Date(
|
||||
Date.UTC(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate()
|
||||
)
|
||||
);
|
||||
// For all-day events, the end date in ICS is exclusive (next day)
|
||||
// The user enters the inclusive end date, so we add 1 day for ICS
|
||||
const utcEnd = new Date(
|
||||
Date.UTC(
|
||||
endDate.getFullYear(),
|
||||
endDate.getMonth(),
|
||||
endDate.getDate() + 1 // Add 1 day for exclusive end
|
||||
)
|
||||
);
|
||||
|
||||
icsEvent = {
|
||||
...eventWithoutDuration,
|
||||
uid: event?.uid || crypto.randomUUID(),
|
||||
summary: title,
|
||||
description: description || undefined,
|
||||
location: location || undefined,
|
||||
stamp: event?.stamp || { date: new Date() },
|
||||
start: {
|
||||
date: utcStart,
|
||||
type: "DATE",
|
||||
},
|
||||
end: {
|
||||
date: utcEnd,
|
||||
type: "DATE",
|
||||
},
|
||||
organizer: organizer,
|
||||
attendees: attendees.length > 0 ? attendees : undefined,
|
||||
recurrenceRule: recurrence,
|
||||
};
|
||||
} else {
|
||||
// Timed event
|
||||
const startDate = parseDateTimeLocal(startDateTime);
|
||||
const endDate = parseDateTimeLocal(endDateTime);
|
||||
|
||||
// Create "fake UTC" dates where getUTCHours() = local hours
|
||||
// This is required because ts-ics uses getUTCHours() to generate ICS
|
||||
const fakeUtcStart = new Date(
|
||||
Date.UTC(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate(),
|
||||
startDate.getHours(),
|
||||
startDate.getMinutes(),
|
||||
startDate.getSeconds()
|
||||
)
|
||||
);
|
||||
const fakeUtcEnd = new Date(
|
||||
Date.UTC(
|
||||
endDate.getFullYear(),
|
||||
endDate.getMonth(),
|
||||
endDate.getDate(),
|
||||
endDate.getHours(),
|
||||
endDate.getMinutes(),
|
||||
endDate.getSeconds()
|
||||
)
|
||||
);
|
||||
|
||||
icsEvent = {
|
||||
...eventWithoutDuration,
|
||||
uid: event?.uid || crypto.randomUUID(),
|
||||
summary: title,
|
||||
description: description || undefined,
|
||||
location: location || undefined,
|
||||
stamp: event?.stamp || { date: new Date() },
|
||||
start: {
|
||||
date: fakeUtcStart,
|
||||
type: "DATE-TIME",
|
||||
local: {
|
||||
date: fakeUtcStart,
|
||||
timezone: BROWSER_TIMEZONE,
|
||||
tzoffset: adapter.getTimezoneOffset(startDate, BROWSER_TIMEZONE),
|
||||
},
|
||||
},
|
||||
end: {
|
||||
date: fakeUtcEnd,
|
||||
type: "DATE-TIME",
|
||||
local: {
|
||||
date: fakeUtcEnd,
|
||||
timezone: BROWSER_TIMEZONE,
|
||||
tzoffset: adapter.getTimezoneOffset(endDate, BROWSER_TIMEZONE),
|
||||
},
|
||||
},
|
||||
organizer: organizer,
|
||||
attendees: attendees.length > 0 ? attendees : undefined,
|
||||
recurrenceRule: recurrence,
|
||||
};
|
||||
}
|
||||
|
||||
await onSave(icsEvent, selectedCalendarUrl);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save event:", error);
|
||||
alert(t('api.error.unexpected'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async (option?: RecurringDeleteOption) => {
|
||||
if (!onDelete || !event?.uid) return;
|
||||
|
||||
setShowDeleteModal(false);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onDelete(event as IcsEvent, selectedCalendarUrl, option);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete event:", error);
|
||||
alert(t('api.error.unexpected'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
const handleRespondToInvitation = async (
|
||||
status: 'ACCEPTED' | 'TENTATIVE' | 'DECLINED'
|
||||
) => {
|
||||
if (!onRespondToInvitation || !event) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onRespondToInvitation(event as IcsEvent, status);
|
||||
// Update local state to reflect new status
|
||||
setAttendees((prev) =>
|
||||
prev.map((att) =>
|
||||
user?.email && att.email.toLowerCase() === user.email.toLowerCase()
|
||||
? { ...att, partstat: status }
|
||||
: att
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to respond to invitation:", error);
|
||||
alert(t('api.error.unexpected'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newStartValue = e.target.value;
|
||||
|
||||
if (isAllDay) {
|
||||
const oldStart = parseDateLocal(startDateTime);
|
||||
const oldEnd = parseDateLocal(endDateTime);
|
||||
const duration = oldEnd.getTime() - oldStart.getTime();
|
||||
|
||||
const newStart = parseDateLocal(newStartValue);
|
||||
const newEnd = new Date(newStart.getTime() + duration);
|
||||
|
||||
setStartDateTime(newStartValue);
|
||||
setEndDateTime(formatDateLocal(newEnd));
|
||||
} else {
|
||||
const oldStart = parseDateTimeLocal(startDateTime);
|
||||
const oldEnd = parseDateTimeLocal(endDateTime);
|
||||
const duration = oldEnd.getTime() - oldStart.getTime();
|
||||
|
||||
const newStart = parseDateTimeLocal(newStartValue);
|
||||
const newEnd = new Date(newStart.getTime() + duration);
|
||||
|
||||
setStartDateTime(newStartValue);
|
||||
setEndDateTime(formatDateTimeLocal(newEnd));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAllDayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newIsAllDay = e.target.checked;
|
||||
setIsAllDay(newIsAllDay);
|
||||
|
||||
// Convert dates when switching modes
|
||||
if (newIsAllDay) {
|
||||
// Convert to date-only format
|
||||
const start = parseDateTimeLocal(startDateTime);
|
||||
const end = parseDateTimeLocal(endDateTime);
|
||||
setStartDateTime(formatDateLocal(start));
|
||||
setEndDateTime(formatDateLocal(end));
|
||||
} else {
|
||||
// Convert to datetime format
|
||||
const start = parseDateLocal(startDateTime);
|
||||
const end = parseDateLocal(endDateTime);
|
||||
setStartDateTime(formatDateTimeLocal(start));
|
||||
setEndDateTime(formatDateTimeLocal(end));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={
|
||||
mode === "create"
|
||||
? t('calendar.event.createTitle')
|
||||
: t('calendar.event.editTitle')
|
||||
}
|
||||
leftActions={
|
||||
mode === "edit" && onDelete ? (
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('calendar.event.delete')}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={
|
||||
<>
|
||||
<Button color="neutral" onClick={onClose} disabled={isLoading}>
|
||||
{t('calendar.event.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color="brand"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || !title.trim()}
|
||||
>
|
||||
{isLoading ? "..." : t('calendar.event.save')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="event-modal__content">
|
||||
<Input
|
||||
label={t('calendar.event.title')}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
{/* Invitation Response Section */}
|
||||
{isInvited && mode === 'edit' && onRespondToInvitation && (
|
||||
<div className="event-modal__invitation">
|
||||
<div className="event-modal__invitation-header">
|
||||
<span className="event-modal__invitation-label">
|
||||
{t('calendar.event.invitation', { defaultValue: 'Invitation' })}
|
||||
</span>
|
||||
<span className="event-modal__invitation-organizer">
|
||||
{t('calendar.event.organizedBy', {
|
||||
defaultValue: 'Organisé par',
|
||||
})}{' '}
|
||||
{event?.organizer?.name || event?.organizer?.email}
|
||||
</span>
|
||||
</div>
|
||||
<div className="event-modal__invitation-actions">
|
||||
<Button
|
||||
size="small"
|
||||
color={
|
||||
currentParticipationStatus === 'ACCEPTED'
|
||||
? 'success'
|
||||
: 'neutral'
|
||||
}
|
||||
onClick={() => handleRespondToInvitation('ACCEPTED')}
|
||||
disabled={
|
||||
isLoading || currentParticipationStatus === 'ACCEPTED'
|
||||
}
|
||||
>
|
||||
✓ {t('calendar.event.accept', { defaultValue: 'Accepter' })}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color={
|
||||
currentParticipationStatus === 'TENTATIVE'
|
||||
? 'warning'
|
||||
: 'neutral'
|
||||
}
|
||||
onClick={() => handleRespondToInvitation('TENTATIVE')}
|
||||
disabled={
|
||||
isLoading || currentParticipationStatus === 'TENTATIVE'
|
||||
}
|
||||
>
|
||||
? {t('calendar.event.maybe', { defaultValue: 'Peut-être' })}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
color={
|
||||
currentParticipationStatus === 'DECLINED'
|
||||
? 'error'
|
||||
: 'neutral'
|
||||
}
|
||||
onClick={() => handleRespondToInvitation('DECLINED')}
|
||||
disabled={
|
||||
isLoading || currentParticipationStatus === 'DECLINED'
|
||||
}
|
||||
>
|
||||
✗ {t('calendar.event.decline', { defaultValue: 'Refuser' })}
|
||||
</Button>
|
||||
</div>
|
||||
{currentParticipationStatus && (
|
||||
<div className="event-modal__invitation-status">
|
||||
{t('calendar.event.yourResponse', {
|
||||
defaultValue: 'Votre réponse',
|
||||
})}
|
||||
:{' '}
|
||||
<strong>
|
||||
{currentParticipationStatus === 'ACCEPTED' &&
|
||||
t('calendar.event.accepted', { defaultValue: 'Accepté' })}
|
||||
{currentParticipationStatus === 'TENTATIVE' &&
|
||||
t('calendar.event.tentative', {
|
||||
defaultValue: 'Peut-être',
|
||||
})}
|
||||
{currentParticipationStatus === 'DECLINED' &&
|
||||
t('calendar.event.declined', { defaultValue: 'Refusé' })}
|
||||
{currentParticipationStatus === 'NEEDS-ACTION' &&
|
||||
t('calendar.event.needsAction', {
|
||||
defaultValue: 'En attente',
|
||||
})}
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label={t('calendar.event.calendar', { defaultValue: 'Calendrier' })}
|
||||
value={selectedCalendarUrl}
|
||||
onChange={(e) => setSelectedCalendarUrl(String(e.target.value))}
|
||||
options={calendars.map((cal) => ({
|
||||
value: cal.url,
|
||||
label: cal.displayName || cal.url,
|
||||
}))}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<label className="event-modal__checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAllDay}
|
||||
onChange={handleAllDayChange}
|
||||
/>
|
||||
<span>{t('calendar.event.allDay')}</span>
|
||||
</label>
|
||||
|
||||
<div className="event-modal__datetime-row">
|
||||
<Input
|
||||
type={isAllDay ? "date" : "datetime-local"}
|
||||
label={t('calendar.event.start')}
|
||||
value={startDateTime}
|
||||
onChange={handleStartDateChange}
|
||||
fullWidth
|
||||
/>
|
||||
<Input
|
||||
type={isAllDay ? "date" : "datetime-local"}
|
||||
label={t('calendar.event.end')}
|
||||
value={endDateTime}
|
||||
onChange={(e) => setEndDateTime(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label={t('calendar.event.location')}
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label={t('calendar.event.description')}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<div className="event-modal__features">
|
||||
<button
|
||||
type="button"
|
||||
className={`event-modal__feature-tag ${
|
||||
showAttendees ? 'event-modal__feature-tag--active' : ''
|
||||
}`}
|
||||
onClick={() => setShowAttendees(!showAttendees)}
|
||||
>
|
||||
<span className="material-icons">people</span>
|
||||
{t('calendar.event.attendees')}
|
||||
{attendees.length > 0 && ` (${attendees.length})`}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`event-modal__feature-tag ${
|
||||
showRecurrence ? 'event-modal__feature-tag--active' : ''
|
||||
}`}
|
||||
onClick={() => setShowRecurrence(!showRecurrence)}
|
||||
>
|
||||
<span className="material-icons">repeat</span>
|
||||
{t('calendar.recurrence.label')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAttendees && (
|
||||
<div className="event-modal__attendees-input">
|
||||
<AttendeesInput
|
||||
attendees={attendees}
|
||||
onChange={setAttendees}
|
||||
organizerEmail={user?.email}
|
||||
organizer={organizer}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRecurrence && (
|
||||
<div className="event-modal__recurrence-editor">
|
||||
<RecurrenceEditor value={recurrence} onChange={setRecurrence} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<DeleteEventModal
|
||||
isOpen={showDeleteModal}
|
||||
isRecurring={!!recurrence}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user