Corp/frontend/src/components/ViewEventDetailsModal.tsx
igor_nadein 89d471f525 refactor: полная унификация всех модальных окон
Проведен аудит всех 6 модальных окон календаря и унифицированы стили.

Проблемы до рефакторинга:
- CalendarModal: z-[200] вместо z-[100], нет backdrop-blur-sm, нет max-h/overflow
- CalendarParticipantsModal: нет backdrop-blur-sm, нет max-h/overflow
- Другие модалы: лишние transition классы

Стандартный стиль для ВСЕХ модалов:

Overlay:
  fixed inset-0 z-[100] flex items-center justify-center
  bg-black/50 backdrop-blur-sm p-4

Контейнер:
  w-full max-w-[md/2xl] rounded-2xl bg-white p-6
  shadow-xl max-h-[90vh] overflow-y-auto

Унифицированные модалы:
✅ EventModal
✅ ViewEventDetailsModal
✅ ViewDayEventsModal
✅ CalendarModal
✅ CalendarParticipantsModal
✅ CalendarSubscriptionsModal

Результат:
- Единообразный внешний вид всех модалов
- Правильная иерархия z-index (все z-[100])
- Размытие фона везде одинаковое
- Прокрутка контента при переполнении
- Простой и понятный код без излишеств
2026-02-25 02:06:00 +03:00

200 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl max-h-[90vh] overflow-y-auto">
<div className="mb-4 flex items-start justify-between">
<div className="flex-1 min-w-0 pr-3">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900">
{event.title}
</h3>
{event.rule && (
<span className="text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded flex-shrink-0">
🔁
</span>
)}
</div>
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<div
className="h-2.5 w-2.5 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 flex-shrink-0"
>
<X size={20} className="text-gray-600" />
</button>
</div>
<div className="space-y-4">
{/* Time */}
<div className="flex items-start gap-2.5">
<Clock size={18} 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-xs text-gray-600">
<span className="font-medium">Начало:</span> {capitalizedStart}
</p>
<p className="text-xs 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-2.5">
<Calendar size={18} 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-xs text-gray-600">
До {formatDate(event.end_recurring_period)}
</p>
</div>
</div>
)}
{/* Description */}
{event.description && (
<div className="flex items-start gap-2.5">
<FileText size={18} 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-xs text-gray-600 whitespace-pre-wrap">
{event.description}
</p>
</div>
</div>
)}
{/* Participants */}
{showParticipants && (
<div className="flex items-start gap-2.5">
<Users size={18} 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.5">
Участники {participants.length > 0 && `(${participants.length})`}
</p>
{loadingParticipants ? (
<p className="text-xs text-gray-500">Загрузка...</p>
) : participants.length > 0 ? (
<div className="space-y-1.5">
{participants.map((participant: any) => (
<div
key={participant.id}
className="flex items-center gap-2 rounded-lg bg-gray-50 px-2.5 py-2"
>
<div className="h-6 w-6 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 className="mt-4 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>
);
}