feat(calendar): улучшенный UX - модалы просмотра перед действиями

Новые компоненты:
- ViewDayEventsModal: показывает все события в выбранный день
  - Список событий с временем и описанием
  - Кнопка "Создать событие на этот день"
  - Клик на событие → ViewEventDetailsModal

- ViewEventDetailsModal: детали события (read-only)
  - Все поля события (время, описание, участники)
  - Индикаторы повторяющихся событий �
  - Кнопки "Редактировать" и "Удалить"

Изменения в BigCalendar:
- handleSelectSlot: клик на дату → открывает ViewDayEventsModal вместо прямого создания
- handleSelectEvent: клик на событие → открывает ViewEventDetailsModal вместо прямого редактирования
- Добавлены обработчики переходов между модалами

Изменения в AppShell:
- handleOpenEventModal: логика маршрутизации модалов (дата/событие/создание)
- Добавлены обработчики для новых модалов
- sidebarEvents: состояние для передачи событий в ViewDayEventsModal
- Интеграция в CalendarCard для обоих календарей (desktop + mobile)

Преимущества UX:
- Пользователь видит контекст перед действием
- Предотвращение случайных дубликатов событий
- Логичный flow: Просмотр → Действие (create/edit/delete)
- Особенно полезно для повторяющихся событий
This commit is contained in:
igor_nadein 2026-02-25 01:04:23 +03:00
parent b490b4e82a
commit be70822945
4 changed files with 587 additions and 36 deletions

View File

@ -10,6 +10,8 @@ import { CalendarProvider, useCalendar } from "@/contexts/CalendarContext";
import { CalendarModal } from "@/components/CalendarModal";
import CalendarParticipantsModal from "@/components/CalendarParticipantsModal";
import { EventModal } from "@/components/EventModal";
import { ViewDayEventsModal } from "@/components/ViewDayEventsModal";
import { ViewEventDetailsModal } from "@/components/ViewEventDetailsModal";
type AppShellProps = {
children: ReactNode;
@ -299,13 +301,15 @@ function CalendarCard({
onOpenEventModal,
onOpenParticipantsModal,
eventsRefreshTrigger,
setEventsRefreshTrigger
setEventsRefreshTrigger,
setSidebarEvents
}: {
onOpenCalendarModal: (calendar?: { id?: number; name: string }) => void;
onOpenEventModal: (event: any, date?: Date) => void;
onOpenParticipantsModal: (calendar: { id: number; name: string; user_role?: string }) => void;
eventsRefreshTrigger: number;
setEventsRefreshTrigger: (value: number | ((prev: number) => number)) => void;
setSidebarEvents: (events: any[]) => void;
}) {
const { calendars, selectedCalendarId, setSelectedCalendarId, loading: calendarsLoading, reloadCalendars } = useCalendar();
@ -353,6 +357,7 @@ function CalendarCard({
if (!cancelled) {
const eventsList = Array.isArray(myEventsResult) ? myEventsResult : (myEventsResult?.results || []);
setEvents(eventsList);
setSidebarEvents(eventsList); // Передаем события наружу
}
return;
}
@ -382,7 +387,9 @@ function CalendarCard({
const recurringOccurrences = occurrencesList.filter((occ: any) => occ.is_recurring);
// Объединяем оба типа событий
setEvents([...regularEvents, ...recurringOccurrences]);
const allEvents = [...regularEvents, ...recurringOccurrences];
setEvents(allEvents);
setSidebarEvents(allEvents); // Передаем события наружу для ViewDayEventsModal
}
} catch (err) {
if (!cancelled) {
@ -803,13 +810,15 @@ function Calendar({
onOpenEventModal,
onOpenParticipantsModal,
eventsRefreshTrigger,
setEventsRefreshTrigger
setEventsRefreshTrigger,
setSidebarEvents
}: {
onOpenCalendarModal: (calendar?: { id?: number; name: string }) => void;
onOpenEventModal: (event: any, date?: Date) => void;
onOpenParticipantsModal: (calendar: { id: number; name: string; user_role?: string }) => void;
eventsRefreshTrigger: number;
setEventsRefreshTrigger: (value: number | ((prev: number) => number)) => void;
setSidebarEvents: (events: any[]) => void;
}) {
return (
<aside className="hidden w-72 flex-shrink-0 space-y-4 lg:block">
@ -820,6 +829,7 @@ function Calendar({
onOpenParticipantsModal={onOpenParticipantsModal}
eventsRefreshTrigger={eventsRefreshTrigger}
setEventsRefreshTrigger={setEventsRefreshTrigger}
setSidebarEvents={setSidebarEvents}
/>
</div>
</aside>
@ -860,6 +870,13 @@ export function AppShell({ children }: AppShellProps) {
const [editingEvent, setEditingEvent] = useState<any>(null);
const [participantsCalendar, setParticipantsCalendar] = useState<{ id: number; name: string; user_role?: string } | null>(null);
const [eventsRefreshTrigger, setEventsRefreshTrigger] = useState(0);
// Новые модалы для просмотра
const [showDayEventsModal, setShowDayEventsModal] = useState(false);
const [selectedDateForModal, setSelectedDateForModal] = useState<Date | null>(null);
const [showEventDetailsModal, setShowEventDetailsModal] = useState(false);
const [viewingEvent, setViewingEvent] = useState<any>(null);
const [sidebarEvents, setSidebarEvents] = useState<any[]>([]); // Для ViewDayEventsModal
// Обработчики модалов
const handleOpenCalendarModal = (calendar?: { id?: number; name: string }) => {
@ -868,10 +885,75 @@ export function AppShell({ children }: AppShellProps) {
};
const handleOpenEventModal = (event: any, date?: Date) => {
setEditingEvent(event);
setShowEventModal(true);
// Если передана дата (клик на дату), открываем модал просмотра дня
if (date && !event.id) {
setSelectedDateForModal(date);
setShowDayEventsModal(true);
}
// Если передано событие с id (клик на событие), открываем детали
else if (event.id) {
setViewingEvent(event);
setShowEventDetailsModal(true);
}
// Fallback - создание нового события
else {
setEditingEvent(event);
setShowEventModal(true);
}
};
// Создание события из модала просмотра дня
const handleCreateEventFromDay = useCallback(() => {
if (!selectedDateForModal) return;
const startDate = new Date(selectedDateForModal);
startDate.setHours(10, 0, 0, 0);
const endDate = new Date(selectedDateForModal);
endDate.setHours(11, 0, 0, 0);
setEditingEvent({
title: "",
description: "",
start: startDate.toISOString(),
end: endDate.toISOString(),
calendar: null,
color_event: "#3498db",
});
setShowDayEventsModal(false);
setShowEventModal(true);
}, [selectedDateForModal]);
// Переход к редактированию из модала просмотра
const handleEditFromDetails = useCallback(() => {
setEditingEvent(viewingEvent);
setShowEventDetailsModal(false);
setShowEventModal(true);
}, [viewingEvent]);
// Клик на событие из модала просмотра дня
const handleEventClickFromDay = useCallback(async (event: any) => {
setShowDayEventsModal(false);
// Если это occurrence, загружаем базовое событие
if (event.is_recurring && event.event_id) {
try {
const fullEvent = await apiClient.getEvent(event.event_id);
setViewingEvent({
...fullEvent,
start: event.start,
end: event.end,
});
setShowEventDetailsModal(true);
} catch (err) {
console.error("Ошибка загрузки события:", err);
}
} else {
setViewingEvent(event);
setShowEventDetailsModal(true);
}
}, []);
const handleEventSaved = useCallback(() => {
// Обновляем список событий в CalendarCard
setEventsRefreshTrigger(prev => prev + 1);
@ -953,6 +1035,7 @@ export function AppShell({ children }: AppShellProps) {
onOpenParticipantsModal={handleOpenParticipantsModal}
eventsRefreshTrigger={eventsRefreshTrigger}
setEventsRefreshTrigger={setEventsRefreshTrigger}
setSidebarEvents={setSidebarEvents}
/>
</div>
@ -1010,6 +1093,7 @@ export function AppShell({ children }: AppShellProps) {
onOpenParticipantsModal={handleOpenParticipantsModal}
eventsRefreshTrigger={eventsRefreshTrigger}
setEventsRefreshTrigger={setEventsRefreshTrigger}
setSidebarEvents={setSidebarEvents}
/>
</div>
</div>
@ -1049,6 +1133,45 @@ export function AppShell({ children }: AppShellProps) {
onSave={handleEventSaved}
showParticipants={true}
/>
{/* Модальное окно просмотра событий дня */}
<ViewDayEventsModal
isOpen={showDayEventsModal}
onClose={() => {
setShowDayEventsModal(false);
setSelectedDateForModal(null);
}}
date={selectedDateForModal}
events={sidebarEvents}
onEventClick={handleEventClickFromDay}
onCreateEvent={handleCreateEventFromDay}
/>
{/* Модальное окно просмотра деталей события */}
<ViewEventDetailsModal
isOpen={showEventDetailsModal}
onClose={() => {
setShowEventDetailsModal(false);
setViewingEvent(null);
}}
event={viewingEvent}
onEdit={handleEditFromDetails}
onDelete={async () => {
if (!viewingEvent?.id) return;
if (!confirm("Удалить это событие?")) return;
try {
await apiClient.deleteEvent(viewingEvent.id);
setShowEventDetailsModal(false);
setViewingEvent(null);
handleEventSaved(); // Обновляем список событий
} catch (err) {
console.error("Ошибка удаления события:", err);
alert("Не удалось удалить событие");
}
}}
showParticipants={true}
/>
</div>
</CalendarProvider>
);

View File

@ -9,6 +9,8 @@ import { useCalendar } from "@/contexts/CalendarContext";
import { X, Plus, Trash2, Settings, ChevronLeft, ChevronRight } from "lucide-react";
import { CalendarModal } from "@/components/CalendarModal";
import { EventModal } from "@/components/EventModal";
import { ViewDayEventsModal } from "@/components/ViewDayEventsModal";
import { ViewEventDetailsModal } from "@/components/ViewEventDetailsModal";
import "react-big-calendar/lib/css/react-big-calendar.css";
import "../app/calendar/calendar.css";
@ -132,6 +134,12 @@ export function BigCalendar() {
const [showCalendarModal, setShowCalendarModal] = useState(false);
const [editingCalendar, setEditingCalendar] = useState<{ id?: number; name: string } | null>(null);
// Новые модалы для просмотра
const [showDayEventsModal, setShowDayEventsModal] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [showEventDetailsModal, setShowEventDetailsModal] = useState(false);
const [viewingEvent, setViewingEvent] = useState<CalendarEvent | null>(null);
// Загрузка событий
const loadEvents = useCallback(async () => {
try {
@ -232,57 +240,97 @@ export function BigCalendar() {
// Обработчики календаря
const handleSelectSlot = useCallback(
({ start, end }: { start: Date; end: Date }) => {
if (!selectedCalendarId) {
alert("Сначала выберите календарь");
return;
}
// Если клик на весь день (00:00 - 00:00), устанавливаем разумное время
const startDate = new Date(start);
const endDate = new Date(end);
// Проверяем, если время 00:00 (клик на день в режиме месяца)
if (startDate.getHours() === 0 && startDate.getMinutes() === 0) {
startDate.setHours(10, 0, 0, 0); // 10:00
endDate.setHours(11, 0, 0, 0); // 11:00
}
setEditingEvent({
title: "",
description: "",
start: startDate,
end: endDate,
calendar: selectedCalendarId,
color_event: "#3498db",
});
setShowEventModal(true);
({ start }: { start: Date; end: Date }) => {
// Открываем модальное окно просмотра событий в этот день
setSelectedDate(start);
setShowDayEventsModal(true);
},
[selectedCalendarId]
[]
);
// Создание события из модала просмотра дня
const handleCreateEventFromDay = useCallback(() => {
if (!selectedCalendarId) {
alert("Сначала выберите календарь");
return;
}
if (!selectedDate) return;
// Устанавливаем время по умолчанию 10:00 - 11:00
const startDate = new Date(selectedDate);
startDate.setHours(10, 0, 0, 0);
const endDate = new Date(selectedDate);
endDate.setHours(11, 0, 0, 0);
setEditingEvent({
title: "",
description: "",
start: startDate,
end: endDate,
calendar: selectedCalendarId,
color_event: "#3498db",
});
// Закрываем модал просмотра дня и открываем модал создания
setShowDayEventsModal(false);
setShowEventModal(true);
}, [selectedCalendarId, selectedDate]);
const handleSelectEvent = useCallback(async (calEvent: CalendarEvent) => {
// Если это occurrence (повторяющееся событие), загружаем базовое событие
if (calEvent.isOccurrence && calEvent.event_id) {
try {
const fullEvent = await apiClient.getEvent(calEvent.event_id);
// Устанавливаем время из occurrence, но остальные данные из базового события
setEditingEvent({
setViewingEvent({
...fullEvent,
start: calEvent.start,
end: calEvent.end,
});
setShowEventModal(true);
setShowEventDetailsModal(true);
} catch (err) {
console.error("Ошибка загрузки базового события:", err);
}
} else {
// Обычное событие - данные уже полные
setEditingEvent(calEvent);
setShowEventModal(true);
// Обычное событие - открываем детали
setViewingEvent(calEvent);
setShowEventDetailsModal(true);
}
}, []);
// Переход к редактированию из модала просмотра
const handleEditFromDetails = useCallback(() => {
setEditingEvent(viewingEvent);
setShowEventDetailsModal(false);
setShowEventModal(true);
}, [viewingEvent]);
// Клик на событие из модала просмотра дня
const handleEventClickFromDay = useCallback(async (event: any) => {
setShowDayEventsModal(false);
// Если это occurrence, загружаем базовое событие
if (event.isOccurrence && event.event_id) {
try {
const fullEvent = await apiClient.getEvent(event.event_id);
setViewingEvent({
...fullEvent,
start: event.start,
end: event.end,
});
setShowEventDetailsModal(true);
} catch (err) {
console.error("Ошибка загрузки события:", err);
}
} else {
setViewingEvent(event);
setShowEventDetailsModal(true);
}
}, []);
// Стилизация событий по цвету
const eventStyleGetter = (event: CalendarEvent) => {
const backgroundColor = event.color_event || "#3498db";
@ -414,6 +462,46 @@ export function BigCalendar() {
}}
showParticipants={true}
/>
{/* Модальное окно просмотра событий дня */}
<ViewDayEventsModal
isOpen={showDayEventsModal}
onClose={() => {
setShowDayEventsModal(false);
setSelectedDate(null);
}}
date={selectedDate}
events={events}
onEventClick={handleEventClickFromDay}
onCreateEvent={handleCreateEventFromDay}
/>
{/* Модальное окно просмотра деталей события */}
<ViewEventDetailsModal
isOpen={showEventDetailsModal}
onClose={() => {
setShowEventDetailsModal(false);
setViewingEvent(null);
}}
event={viewingEvent}
onEdit={handleEditFromDetails}
onDelete={async () => {
if (!viewingEvent?.id) return;
if (!confirm("Удалить это событие?")) return;
try {
await apiClient.deleteEvent(viewingEvent.id);
setShowEventDetailsModal(false);
setViewingEvent(null);
loadEvents();
} catch (err) {
console.error("Ошибка удаления события:", err);
alert("Не удалось удалить событие");
}
}}
showParticipants={true}
/>
{/* Модальное окно управления календарем */}
<CalendarModal
isOpen={showCalendarModal}

View File

@ -0,0 +1,136 @@
"use client";
import { X, Plus, Clock, Calendar } from "lucide-react";
import { format } from "date-fns";
import { ru } from "date-fns/locale";
interface ViewDayEventsModalProps {
isOpen: boolean;
onClose: () => void;
date: Date | null;
events: any[];
onEventClick: (event: any) => void;
onCreateEvent: () => void;
}
export function ViewDayEventsModal({
isOpen,
onClose,
date,
events,
onEventClick,
onCreateEvent,
}: ViewDayEventsModalProps) {
if (!isOpen || !date) return null;
const formattedDate = format(date, "d MMMM yyyy, EEEE", { locale: ru });
const capitalizedDate = formattedDate.charAt(0).toUpperCase() + formattedDate.slice(1);
const dayEvents = events.filter(event => {
const eventDate = new Date(event.start);
return eventDate.toDateString() === date.toDateString();
});
// Сортируем события по времени начала
const sortedEvents = [...dayEvents].sort((a, b) => {
return new Date(a.start).getTime() - new Date(b.start).getTime();
});
const formatTime = (dateStr: string) => {
const date = new Date(dateStr);
return format(date, "HH:mm", { locale: ru });
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-2xl rounded-2xl bg-white shadow-xl max-h-[85vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">События на {capitalizedDate}</h3>
<p className="text-sm text-gray-500 mt-0.5">
{sortedEvents.length === 0
? "Нет событий"
: `${sortedEvents.length} ${sortedEvents.length === 1 ? "событие" : "событий"}`}
</p>
</div>
<button
onClick={onClose}
className="rounded-full p-1 hover:bg-gray-100 transition"
>
<X size={20} className="text-gray-600" />
</button>
</div>
{/* Events List */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{sortedEvents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Calendar size={48} className="text-gray-300 mb-3" />
<p className="text-gray-500 text-sm mb-1">Нет событий в этот день</p>
<p className="text-gray-400 text-xs">Создайте новое событие, нажав кнопку ниже</p>
</div>
) : (
<div className="space-y-2">
{sortedEvents.map((event) => (
<button
key={event.id}
onClick={() => onEventClick(event)}
className="w-full text-left rounded-lg border border-gray-200 bg-white p-4 transition hover:border-sky-300 hover:bg-sky-50/50 hover:shadow-sm"
>
<div className="flex items-start gap-3">
{/* Color Indicator */}
<div
className="mt-1 h-10 w-1 rounded-full flex-shrink-0"
style={{ backgroundColor: event.color_event || "#3498db" }}
/>
<div className="flex-1 min-w-0">
{/* Title with recurring indicator */}
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-gray-900 truncate">
{event.title}
</h4>
{event.rule && (
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded flex-shrink-0">
🔁 Повторяется
</span>
)}
</div>
{/* Time */}
<div className="flex items-center gap-1.5 text-sm text-gray-600">
<Clock size={14} />
<span>
{formatTime(event.start)} - {formatTime(event.end)}
</span>
</div>
{/* Description preview */}
{event.description && (
<p className="text-xs text-gray-500 mt-2 line-clamp-2">
{event.description}
</p>
)}
</div>
</div>
</button>
))}
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-gray-200 px-6 py-4">
<button
onClick={onCreateEvent}
className="w-full flex items-center justify-center gap-2 rounded-lg bg-sky-500 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-sky-600"
>
<Plus size={16} />
Создать событие на этот день
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,204 @@
"use client";
import { X, Edit2, Trash2, Clock, Calendar, FileText, Users } from "lucide-react";
import { format } from "date-fns";
import { ru } from "date-fns/locale";
import { useState, useEffect } from "react";
import { apiClient } from "@/lib/api";
interface ViewEventDetailsModalProps {
isOpen: boolean;
onClose: () => void;
event: any | null;
onEdit: () => void;
onDelete: () => void;
showParticipants?: boolean;
}
export function ViewEventDetailsModal({
isOpen,
onClose,
event,
onEdit,
onDelete,
showParticipants = false,
}: ViewEventDetailsModalProps) {
const [participants, setParticipants] = useState<any[]>([]);
const [loadingParticipants, setLoadingParticipants] = useState(false);
useEffect(() => {
if (isOpen && event?.id && showParticipants) {
loadParticipants();
} else {
setParticipants([]);
}
}, [isOpen, event?.id, showParticipants]);
const loadParticipants = async () => {
if (!event?.id) return;
try {
setLoadingParticipants(true);
const result = await apiClient.getEventParticipants(event.id);
const participantsList = Array.isArray(result) ? result : (result?.results || []);
setParticipants(participantsList);
} catch (error) {
console.error("Failed to load participants:", error);
} finally {
setLoadingParticipants(false);
}
};
if (!isOpen || !event) return null;
const formatDateTime = (dateStr: string) => {
const date = new Date(dateStr);
return format(date, "d MMMM yyyy, HH:mm", { locale: ru });
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return format(date, "d MMMM yyyy", { locale: ru });
};
const capitalizedStart = formatDateTime(event.start).charAt(0).toUpperCase() + formatDateTime(event.start).slice(1);
const capitalizedEnd = formatDateTime(event.end).charAt(0).toUpperCase() + formatDateTime(event.end).slice(1);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-2xl rounded-2xl bg-white shadow-xl max-h-[85vh] flex flex-col">
{/* Header */}
<div className="flex items-start justify-between border-b border-gray-200 px-6 py-4">
<div className="flex-1 min-w-0 pr-4">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{event.title}
</h3>
{event.rule && (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded flex-shrink-0">
🔁 Повторяется
</span>
)}
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500">
<div
className="h-3 w-3 rounded-full flex-shrink-0"
style={{ backgroundColor: event.color_event || "#3498db" }}
/>
<span className="truncate">Календарь #{event.calendar}</span>
</div>
</div>
<button
onClick={onClose}
className="rounded-full p-1 hover:bg-gray-100 transition flex-shrink-0"
>
<X size={20} className="text-gray-600" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{/* Time */}
<div className="flex items-start gap-3">
<Clock size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-gray-700">Время</p>
<p className="text-sm text-gray-600 mt-1">
<span className="font-medium">Начало:</span> {capitalizedStart}
</p>
<p className="text-sm text-gray-600 mt-0.5">
<span className="font-medium">Конец:</span> {capitalizedEnd}
</p>
</div>
</div>
{/* Recurring Info */}
{event.rule && event.end_recurring_period && (
<div className="flex items-start gap-3">
<Calendar size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-gray-700">Повторение</p>
<p className="text-sm text-gray-600 mt-1">
До {formatDate(event.end_recurring_period)}
</p>
</div>
</div>
)}
{/* Description */}
{event.description && (
<div className="flex items-start gap-3">
<FileText size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-gray-700 mb-1">Описание</p>
<p className="text-sm text-gray-600 whitespace-pre-wrap">
{event.description}
</p>
</div>
</div>
)}
{/* Participants */}
{showParticipants && (
<div className="flex items-start gap-3">
<Users size={20} className="text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-gray-700 mb-2">
Участники {participants.length > 0 && `(${participants.length})`}
</p>
{loadingParticipants ? (
<p className="text-xs text-gray-500">Загрузка...</p>
) : participants.length > 0 ? (
<div className="space-y-2">
{participants.map((participant: any) => (
<div
key={participant.id}
className="flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-2"
>
<div className="h-7 w-7 rounded-full bg-sky-100 flex items-center justify-center flex-shrink-0">
<span className="text-xs font-medium text-sky-700">
{participant.user_name?.[0] || "?"}
</span>
</div>
<div className="text-xs flex-1 min-w-0">
<div className="font-medium text-gray-900 truncate">
{participant.user_name}
</div>
<div className="text-gray-500">
{participant.distinction || "attendee"}
</div>
</div>
</div>
))}
</div>
) : (
<p className="text-xs text-gray-500">Нет участников</p>
)}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-gray-200 px-6 py-4">
<div className="flex gap-2">
<button
onClick={onEdit}
className="flex-1 flex items-center justify-center gap-2 rounded-lg bg-sky-500 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-sky-600"
>
<Edit2 size={16} />
Редактировать
</button>
<button
onClick={onDelete}
className="rounded-lg border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 transition hover:bg-red-100"
title="Удалить событие"
>
<Trash2 size={16} />
</button>
</div>
</div>
</div>
</div>
);
}