'use client' import { useState, useEffect, useMemo } from 'react' import { ChevronLeft, ChevronRight, Plus, X, Clock, MapPin, Trash2, Eye, EyeOff, CalendarDays, User, AlignLeft, List } from 'lucide-react' interface CalendarEvent { id: string title: string start: string end: string allDay: boolean description: string | null location: string | null owner: string ownerName: string color: string } const WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] const MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'] function formatTime(iso: string): string { return new Date(iso).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) } function formatFullDate(iso: string): string { return new Date(iso).toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }) } // ————— Add Event Modal ————— const CALENDAR_OPTIONS = [ { owner: 'daniil', name: 'Даниил', color: '#6366f1' }, { owner: 'sveta', name: 'Света', color: '#ec4899' }, ] function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string; onClose: () => void; onSaved: (e: any) => void }) { const [title, setTitle] = useState('') const [date, setDate] = useState(defaultDate) const [startTime, setStartTime] = useState('10:00') const [endTime, setEndTime] = useState('11:00') const [allDay, setAllDay] = useState(false) const [owner, setOwner] = useState('daniil') const [saving, setSaving] = useState(false) const [error, setError] = useState('') const selectedCal = CALENDAR_OPTIONS.find(c => c.owner === owner) || CALENDAR_OPTIONS[0] const save = async () => { if (!title.trim()) { setError('Введите название'); return } setSaving(true); setError('') try { const body = { title: title.trim(), date, startTime: allDay ? null : startTime, endTime: allDay ? null : endTime, allDay, owner } const r = await fetch('/api/calendar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) const d = await r.json() if (d.error) throw new Error(d.error) onSaved(d.event) } catch (e: any) { setError(e.message || 'Ошибка сохранения'); setSaving(false) } } return (
e.stopPropagation()}> {/* Header */}
Новое событие
{/* Body */}
{/* Calendar selector */}
{CALENDAR_OPTIONS.map(cal => { const sel = owner === cal.owner return ( ) })}
{/* Title */} setTitle(e.target.value)} placeholder="Название события" autoFocus style={{ width: '100%', padding: '15px 18px', borderRadius: 14, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', color: 'var(--text-primary)', fontSize: 16, fontWeight: 500, outline: 'none', fontFamily: 'inherit', marginBottom: 16, }} /> {/* Date */}
Дата
setDate(e.target.value)} style={{ width: '100%', padding: '14px 18px', borderRadius: 14, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', color: 'var(--text-primary)', fontSize: 15, outline: 'none', fontFamily: 'inherit', }} />
{/* All day toggle */} {/* Time pickers */} {!allDay && (
Время
setStartTime(e.target.value)} style={{ flex: 1, padding: '14px 18px', border: 'none', background: 'transparent', color: 'var(--text-primary)', fontSize: 16, fontWeight: 600, fontFamily: 'inherit', outline: 'none', textAlign: 'center', }} />
до
setEndTime(e.target.value)} style={{ flex: 1, padding: '14px 18px', border: 'none', background: 'transparent', color: 'var(--text-primary)', fontSize: 16, fontWeight: 600, fontFamily: 'inherit', outline: 'none', textAlign: 'center', }} />
)} {/* Error */} {error && (
{error}
)} {/* Submit */}
) } // ————— Event Detail Modal ————— function EventDetailModal({ event, onClose, onDelete, onUpdate }: { event: CalendarEvent onClose: () => void onDelete: (e: CalendarEvent) => Promise onUpdate: (old: CalendarEvent, updated: Partial) => Promise }) { const [confirmDelete, setConfirmDelete] = useState(false) const [deleting, setDeleting] = useState(false) const [editing, setEditing] = useState(false) const [editTitle, setEditTitle] = useState(event.title) const [editDate, setEditDate] = useState( event.start.includes('T') ? event.start.split('T')[0] : event.start ) const [editStartTime, setEditStartTime] = useState( event.start.includes('T') ? formatTime(event.start) : '10:00' ) const [editEndTime, setEditEndTime] = useState( event.end.includes('T') ? formatTime(event.end) : '11:00' ) const [editAllDay, setEditAllDay] = useState(event.allDay) const [saving, setSaving] = useState(false) const [saveError, setSaveError] = useState('') const handleDelete = async () => { setDeleting(true) await onDelete(event) setDeleting(false) } const handleSave = async () => { if (!editTitle.trim()) { setSaveError('Введите название'); return } setSaving(true); setSaveError('') try { await onUpdate(event, { title: editTitle.trim(), start: editAllDay ? editDate : `${editDate}T${editStartTime}`, end: editAllDay ? editDate : `${editDate}T${editEndTime}`, allDay: editAllDay, }) } catch (e: any) { setSaveError(e.message || 'Ошибка сохранения') setSaving(false) } } return (
e.stopPropagation()}> {/* Colored header */}
{editing ? ( setEditTitle(e.target.value)} autoFocus style={{ width: '100%', padding: '10px 14px', borderRadius: 12, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-primary)', fontSize: 18, fontWeight: 700, outline: 'none', fontFamily: 'inherit', }} /> ) : (
{event.title}
)}
{event.ownerName}
{!editing && ( )}
{/* Content */}
{editing ? ( <> {/* Date */}
Дата
setEditDate(e.target.value)} style={{ width: '100%', padding: '12px 16px', borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', color: 'var(--text-primary)', fontSize: 15, outline: 'none', fontFamily: 'inherit', }} />
{/* All day toggle */} {/* Time */} {!editAllDay && (
Время
setEditStartTime(e.target.value)} style={{ flex: 1, padding: '12px 16px', border: 'none', background: 'transparent', color: 'var(--text-primary)', fontSize: 16, fontWeight: 600, fontFamily: 'inherit', outline: 'none', textAlign: 'center', }} />
до
setEditEndTime(e.target.value)} style={{ flex: 1, padding: '12px 16px', border: 'none', background: 'transparent', color: 'var(--text-primary)', fontSize: 16, fontWeight: 600, fontFamily: 'inherit', outline: 'none', textAlign: 'center', }} />
)} {saveError && (
{saveError}
)} {/* Save / Cancel */}
) : ( <> {/* View mode */}
{formatFullDate(event.start)}
{event.allDay ? 'Весь день' : `${formatTime(event.start)} — ${formatTime(event.end)}`}
{event.location && (
{event.location}
)} {event.description && (
Описание
{event.description}
)} {/* Delete */}
{!confirmDelete ? ( ) : (
)}
)}
) } // ————— Day Events Popover ————— function DayEventsModal({ day, month, year, events, onClose, onSelect }: { day: number; month: number; year: number events: CalendarEvent[] onClose: () => void onSelect: (e: CalendarEvent) => void }) { const date = new Date(year, month, day) const label = date.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' }) return (
e.stopPropagation()}>
{label}
{events.map(e => ( ))}
) } // ————— Main Calendar ————— export default function CalendarTab() { const [year, setYear] = useState(new Date().getFullYear()) const [month, setMonth] = useState(new Date().getMonth()) const [events, setEvents] = useState([]) const [loading, setLoading] = useState(true) const [selectedEvent, setSelectedEvent] = useState(null) const [showAddModal, setShowAddModal] = useState(false) const [addDate, setAddDate] = useState('') const [hiddenOwners, setHiddenOwners] = useState>(new Set()) const [dayPopover, setDayPopover] = useState<{ day: number; events: CalendarEvent[] } | null>(null) const [showUpcoming, setShowUpcoming] = useState(false) useEffect(() => { setLoading(true) fetch('/api/calendar?range=month&year=' + year + '&month=' + month) .then(r => r.json()) .then(d => { setEvents(d.events || []); setLoading(false) }) .catch(() => setLoading(false)) }, [year, month]) const deleteEvent = async (event: CalendarEvent) => { try { const r = await fetch(`/api/calendar?eventId=${event.id}`, { method: 'DELETE' }) const d = await r.json() if (d.error) throw new Error(d.error) setEvents(prev => prev.filter(e => e.id !== event.id)) setSelectedEvent(null) } catch (e: any) { alert(e.message || 'Ошибка удаления') } } const calendarOwners = useMemo(() => { const map = new Map() events.forEach(e => { const existing = map.get(e.owner) if (existing) existing.count++ else map.set(e.owner, { owner: e.owner, ownerName: e.ownerName, color: e.color, count: 1 }) }) return Array.from(map.values()) }, [events]) const toggleOwner = (owner: string) => { setHiddenOwners(prev => { const next = new Set(prev) if (next.has(owner)) next.delete(owner) else next.add(owner) return next }) } const filteredEvents = useMemo(() => { if (hiddenOwners.size === 0) return events return events.filter(e => !hiddenOwners.has(e.owner)) }, [events, hiddenOwners]) const upcoming = filteredEvents .filter(e => new Date(e.start) >= new Date()) .slice(0, 6) const firstDay = new Date(year, month, 1) const lastDay = new Date(year, month + 1, 0) const startOffset = (firstDay.getDay() + 6) % 7 const totalCells = Math.ceil((startOffset + lastDay.getDate()) / 7) * 7 const cells: (number | null)[] = [] for (let i = 0; i < totalCells; i++) { const dayNum = i - startOffset + 1 cells.push(dayNum >= 1 && dayNum <= lastDay.getDate() ? dayNum : null) } const getEventsForDay = (day: number) => { return filteredEvents.filter(e => { const d = new Date(e.start) return d.getFullYear() === year && d.getMonth() === month && d.getDate() === day }) } const prevMonth = () => { if (month === 0) { setMonth(11); setYear(y => y - 1) } else setMonth(m => m - 1) } const nextMonth = () => { if (month === 11) { setMonth(0); setYear(y => y + 1) } else setMonth(m => m + 1) } const today = new Date() const handleDayClick = (day: number) => { const dayEvents = getEventsForDay(day) if (dayEvents.length === 0) { setAddDate(`${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`) setShowAddModal(true) } else if (dayEvents.length === 1) { setSelectedEvent(dayEvents[0]) } else { setDayPopover({ day, events: dayEvents }) } } return (
{/* Main calendar grid */}
{/* Header */}
{MONTHS[month]} {year}
{calendarOwners.map(cal => { const isHidden = hiddenOwners.has(cal.owner) return ( ) })}
{/* Weekday headers */}
{WEEKDAYS.map((d, i) => (
= 5 ? 'rgba(248,113,113,0.5)' : 'var(--text-secondary)', textTransform: 'uppercase', padding: '6px 0', letterSpacing: '0.05em', }}>{d}
))}
{/* Calendar cells */}
{cells.map((day, idx) => { if (!day) return
const dayEvents = getEventsForDay(day) const isToday = today.getFullYear() === year && today.getMonth() === month && today.getDate() === day const dayOfWeek = (startOffset + day - 1) % 7 const isWeekend = dayOfWeek >= 5 return (
handleDayClick(day)} style={{ borderRadius: 12, padding: '4px 5px', background: isToday ? 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.08))' : 'rgba(255,255,255,0.015)', border: isToday ? '1px solid rgba(129,140,248,0.3)' : '1px solid transparent', cursor: 'pointer', minHeight: 70, display: 'flex', flexDirection: 'column', gap: 3, transition: 'all 0.2s ease', }} > {day} {dayEvents.slice(0, 3).map(e => (
{ ev.stopPropagation(); setSelectedEvent(e) }} style={{ display: 'flex', alignItems: 'center', gap: 4, background: `${e.color}18`, borderLeft: `3px solid ${e.color}`, borderRadius: '0 6px 6px 0', padding: '4px 6px', cursor: 'pointer', overflow: 'hidden', }} > {!e.allDay && ( {formatTime(e.start)} )} {e.title}
))} {dayEvents.length > 3 && (
+{dayEvents.length - 3}
)}
) })}
{/* Right panel — overlay */} {showUpcoming &&
setShowUpcoming(false)} />}
Ближайшие
{upcoming.length === 0 && !loading && (
Нет событий
)} {upcoming.map(e => { const d = new Date(e.start) return (
setSelectedEvent(e)} style={{ borderRadius: 16, padding: '14px', background: `${e.color}0c`, border: `1px solid ${e.color}1a`, cursor: 'pointer', transition: 'all 0.25s ease', }}>
{d.getDate()}
{MONTHS[d.getMonth()].slice(0, 3)}
{e.allDay ? 'Весь день' : formatTime(e.start)}
{e.title}
{e.ownerName}
) })}
{/* Modals */} {selectedEvent && ( setSelectedEvent(null)} onDelete={deleteEvent} onUpdate={updateEvent} /> )} {dayPopover && ( setDayPopover(null)} onSelect={e => { setDayPopover(null); setSelectedEvent(e) }} /> )} {showAddModal && ( setShowAddModal(false)} onSaved={(newEvent) => { setEvents(prev => [...prev, newEvent]); setShowAddModal(false) }} /> )}
) }