Files
smart-home-tablet/components/CalendarTab.tsx
Cosmo d29deedc90
Some checks failed
Deploy / deploy (push) Has been cancelled
fix: upcoming panel as slide-out overlay to prevent layout overflow on tablets
2026-04-22 19:30:03 +00:00

637 lines
30 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 { 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 —————
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 [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const inputStyle: React.CSSProperties = {
padding: '14px 18px', borderRadius: 14,
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)',
color: 'var(--text-primary)', fontSize: 15, outline: 'none', fontFamily: 'inherit',
width: '100%',
}
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 }
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 (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(12px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={onClose}>
<div style={{
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.08)', borderRadius: 28,
padding: '32px 36px', width: 440, maxWidth: '95vw',
boxShadow: '0 25px 80px rgba(0,0,0,0.6)',
}} onClick={e => e.stopPropagation()}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 28 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 40, height: 40, borderRadius: 12,
background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<Plus size={20} color="#a5b4fc" />
</div>
<span style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)' }}>Новое событие</span>
</div>
<button onClick={onClose} style={{ color: 'var(--text-secondary)', padding: 6, borderRadius: 10, background: 'rgba(255,255,255,0.04)' }}><X size={18} /></button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Title */}
<div>
<label style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600, marginBottom: 6, display: 'block', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Название</label>
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="Встреча, звонок, задача..." autoFocus style={inputStyle} />
</div>
{/* Date */}
<div>
<label style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600, marginBottom: 6, display: 'block', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Дата</label>
<input type="date" value={date} onChange={e => setDate(e.target.value)} style={inputStyle} />
</div>
{/* Time */}
{!allDay && (
<div>
<label style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600, marginBottom: 6, display: 'block', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Время</label>
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
<input type="time" value={startTime} onChange={e => setStartTime(e.target.value)} style={{ ...inputStyle, flex: 1 }} />
<span style={{ color: 'var(--text-secondary)', fontSize: 14, flexShrink: 0 }}></span>
<input type="time" value={endTime} onChange={e => setEndTime(e.target.value)} style={{ ...inputStyle, flex: 1 }} />
</div>
</div>
)}
{/* All day toggle */}
<label style={{
display: 'flex', alignItems: 'center', gap: 10,
color: 'var(--text-secondary)', fontSize: 14, cursor: 'pointer',
padding: '10px 16px', borderRadius: 12,
background: allDay ? 'rgba(99,102,241,0.08)' : 'rgba(255,255,255,0.02)',
border: allDay ? '1px solid rgba(129,140,248,0.15)' : '1px solid rgba(255,255,255,0.05)',
transition: 'all 0.2s ease',
}}>
<input type="checkbox" checked={allDay} onChange={e => setAllDay(e.target.checked)} style={{ accentColor: '#818cf8' }} />
<span style={{ fontWeight: 500 }}>Весь день</span>
</label>
{error && <div style={{ color: '#f87171', fontSize: 13, padding: '8px 12px', borderRadius: 10, background: 'rgba(239,68,68,0.08)' }}>{error}</div>}
<button onClick={save} disabled={saving} style={{
padding: '15px', borderRadius: 16, marginTop: 4,
background: saving ? 'rgba(99,102,241,0.15)' : 'linear-gradient(135deg, rgba(99,102,241,0.4), rgba(139,92,246,0.3))',
border: '1px solid rgba(129,140,248,0.3)', color: '#a5b4fc',
fontSize: 15, fontWeight: 700, cursor: saving ? 'default' : 'pointer',
transition: 'all 0.25s ease',
}}>
{saving ? 'Сохранение...' : 'Создать событие'}
</button>
</div>
</div>
</div>
)
}
// ————— Event Detail Modal —————
function EventDetailModal({ event, onClose, onDelete }: {
event: CalendarEvent
onClose: () => void
onDelete: (e: CalendarEvent) => Promise<void>
}) {
const [confirmDelete, setConfirmDelete] = useState(false)
const [deleting, setDeleting] = useState(false)
const handleDelete = async () => {
setDeleting(true)
await onDelete(event)
setDeleting(false)
}
const startDate = new Date(event.start)
const endDate = new Date(event.end)
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(12px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={onClose}>
<div style={{
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.08)', borderRadius: 28,
padding: 0, width: 480, maxWidth: '95vw',
boxShadow: '0 25px 80px rgba(0,0,0,0.6)', overflow: 'hidden',
}} onClick={e => e.stopPropagation()}>
{/* Colored header band */}
<div style={{
background: `linear-gradient(135deg, ${event.color}25, ${event.color}10)`,
borderBottom: `1px solid ${event.color}20`,
padding: '28px 32px 24px',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 22, fontWeight: 700, color: 'var(--text-primary)',
lineHeight: 1.3, marginBottom: 8,
}}>
{event.title}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 8, height: 8, borderRadius: '50%', background: event.color, flexShrink: 0,
boxShadow: `0 0 8px ${event.color}60`,
}} />
<span style={{ fontSize: 14, color: event.color, fontWeight: 600 }}>{event.ownerName}</span>
</div>
</div>
<button onClick={onClose} style={{
color: 'var(--text-secondary)', padding: 8, borderRadius: 12,
background: 'rgba(255,255,255,0.06)', flexShrink: 0, marginLeft: 12,
}}>
<X size={18} />
</button>
</div>
</div>
{/* Content */}
<div style={{ padding: '24px 32px 28px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Date & Time */}
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 14,
padding: '16px 18px', borderRadius: 16,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
}}>
<div style={{
width: 42, height: 42, borderRadius: 14,
background: `${event.color}15`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<CalendarDays size={20} color={event.color} />
</div>
<div>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', textTransform: 'capitalize' }}>
{formatFullDate(event.start)}
</div>
<div style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4 }}>
{event.allDay
? 'Весь день'
: `${formatTime(event.start)}${formatTime(event.end)}`
}
</div>
</div>
</div>
{/* Location */}
{event.location && (
<div style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '16px 18px', borderRadius: 16,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
}}>
<div style={{
width: 42, height: 42, borderRadius: 14,
background: 'rgba(251,146,60,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<MapPin size={20} color="#fb923c" />
</div>
<div style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 500 }}>{event.location}</div>
</div>
)}
{/* Description */}
{event.description && (
<div style={{
padding: '16px 18px', borderRadius: 16,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<AlignLeft size={15} color="var(--text-secondary)" />
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Описание</span>
</div>
<div style={{
fontSize: 14, color: 'var(--text-primary)', lineHeight: 1.7,
whiteSpace: 'pre-wrap', fontWeight: 400,
}}>
{event.description}
</div>
</div>
)}
{/* Delete */}
<div style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: 16, marginTop: 4 }}>
{!confirmDelete ? (
<button onClick={() => setConfirmDelete(true)} style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
padding: '13px 18px', borderRadius: 14, width: '100%',
background: 'rgba(239,68,68,0.06)', border: '1px solid rgba(239,68,68,0.15)',
color: '#f87171', fontSize: 14, fontWeight: 600,
transition: 'all 0.25s ease',
}}>
<Trash2 size={16} /> Удалить событие
</button>
) : (
<div style={{ display: 'flex', gap: 10 }}>
<button onClick={handleDelete} disabled={deleting} style={{
flex: 1, padding: '13px 18px', borderRadius: 14,
background: deleting ? 'rgba(239,68,68,0.08)' : 'rgba(239,68,68,0.15)',
border: '1px solid rgba(239,68,68,0.3)',
color: '#f87171', fontSize: 14, fontWeight: 600,
}}>
{deleting ? 'Удаление...' : 'Да, удалить'}
</button>
<button onClick={() => setConfirmDelete(false)} style={{
flex: 1, padding: '13px 18px', borderRadius: 14,
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
color: 'var(--text-secondary)', fontSize: 14, fontWeight: 600,
}}>
Отмена
</button>
</div>
)}
</div>
</div>
</div>
</div>
)
}
// ————— 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 (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(8px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={onClose}>
<div style={{
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.08)', borderRadius: 24,
padding: '24px 28px', width: 400, maxWidth: '95vw',
boxShadow: '0 25px 60px rgba(0,0,0,0.5)',
}} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
<span style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)', textTransform: 'capitalize' }}>{label}</span>
<button onClick={onClose} style={{ color: 'var(--text-secondary)', padding: 4 }}><X size={18} /></button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{events.map(e => (
<button key={e.id} onClick={() => onSelect(e)} style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '14px 16px', borderRadius: 14, width: '100%', textAlign: 'left',
background: `${e.color}0c`, border: `1px solid ${e.color}20`,
transition: 'all 0.2s ease',
}}>
<div style={{ width: 4, borderRadius: 2, background: e.color, alignSelf: 'stretch', minHeight: 36, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{e.title}</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 3, display: 'flex', gap: 8 }}>
<span>{e.allDay ? 'Весь день' : `${formatTime(e.start)}${formatTime(e.end)}`}</span>
<span style={{ color: e.color, fontWeight: 500 }}>{e.ownerName}</span>
</div>
</div>
</button>
))}
</div>
</div>
</div>
)
}
// ————— Main Calendar —————
export default function CalendarTab() {
const [year, setYear] = useState(new Date().getFullYear())
const [month, setMonth] = useState(new Date().getMonth())
const [events, setEvents] = useState<CalendarEvent[]>([])
const [loading, setLoading] = useState(true)
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
const [showAddModal, setShowAddModal] = useState(false)
const [addDate, setAddDate] = useState<string>('')
const [hiddenOwners, setHiddenOwners] = useState<Set<string>>(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<string, { owner: string; ownerName: string; color: string; count: number }>()
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 (
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', padding: '16px 24px 24px', position: 'relative' }}>
{/* Main calendar grid */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<button onClick={prevMonth} style={{ width: 36, height: 36, borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)', color: 'var(--text-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ChevronLeft size={16} />
</button>
<span style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)', minWidth: 180, textAlign: 'center' }}>
{MONTHS[month]} {year}
</span>
<button onClick={nextMonth} style={{ width: 36, height: 36, borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)', color: 'var(--text-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ChevronRight size={16} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{calendarOwners.map(cal => {
const isHidden = hiddenOwners.has(cal.owner)
return (
<button key={cal.owner} onClick={() => toggleOwner(cal.owner)} style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '7px 14px', borderRadius: 12,
background: isHidden ? 'rgba(255,255,255,0.02)' : `${cal.color}15`,
border: `1px solid ${isHidden ? 'rgba(255,255,255,0.06)' : cal.color + '30'}`,
color: isHidden ? 'var(--text-tertiary)' : cal.color,
fontSize: 12, fontWeight: 600, transition: 'all 0.25s ease',
opacity: isHidden ? 0.5 : 1,
}}>
{isHidden ? <EyeOff size={13} /> : <Eye size={13} />}
{cal.ownerName}
<span style={{ fontSize: 10, padding: '1px 6px', borderRadius: 6, background: isHidden ? 'rgba(255,255,255,0.05)' : `${cal.color}20` }}>
{cal.count}
</span>
</button>
)
})}
<button onClick={() => setShowUpcoming(v => !v)} style={{
width: 36, height: 36, borderRadius: 12,
background: showUpcoming ? 'rgba(99,102,241,0.15)' : 'rgba(255,255,255,0.04)',
border: showUpcoming ? '1px solid rgba(129,140,248,0.25)' : '1px solid rgba(255,255,255,0.06)',
color: showUpcoming ? '#a5b4fc' : 'var(--text-secondary)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 0.25s ease',
}}>
<List size={16} />
</button>
<button onClick={() => { setAddDate(today.toISOString().split('T')[0]); setShowAddModal(true) }} style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 18px', borderRadius: 14,
background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))',
border: '1px solid rgba(129,140,248,0.25)',
color: '#a5b4fc', fontSize: 13, fontWeight: 600,
}}>
<Plus size={15} /> Событие
</button>
</div>
</div>
{/* Weekday headers */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', marginBottom: 4 }}>
{WEEKDAYS.map((d, i) => (
<div key={d} style={{
textAlign: 'center', fontSize: 11, fontWeight: 600,
color: i >= 5 ? 'rgba(248,113,113,0.5)' : 'var(--text-secondary)',
textTransform: 'uppercase', padding: '6px 0', letterSpacing: '0.05em',
}}>{d}</div>
))}
</div>
{/* Calendar cells */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', flex: 1, gap: 3 }}>
{cells.map((day, idx) => {
if (!day) return <div key={idx} />
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 (
<div
key={idx}
onClick={() => 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',
}}
>
<span style={{
fontSize: 12, fontWeight: isToday ? 700 : 500,
color: isToday ? '#a5b4fc' : isWeekend ? 'rgba(248,113,113,0.6)' : 'var(--text-secondary)',
textAlign: 'right', paddingRight: 3, lineHeight: 1,
}}>{day}</span>
{dayEvents.slice(0, 3).map(e => (
<div
key={e.id}
onClick={ev => { 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 && (
<span style={{ fontSize: 9, color: `${e.color}cc`, fontWeight: 600, flexShrink: 0, fontVariantNumeric: 'tabular-nums' }}>
{formatTime(e.start)}
</span>
)}
<span style={{
fontSize: 11, fontWeight: 600, color: e.color,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{e.title}
</span>
</div>
))}
{dayEvents.length > 3 && (
<div style={{ fontSize: 10, color: 'var(--text-secondary)', textAlign: 'center', fontWeight: 500 }}>
+{dayEvents.length - 3}
</div>
)}
</div>
)
})}
</div>
</div>
{/* Right panel — overlay */}
{showUpcoming && <div style={{ position: 'fixed', inset: 0, zIndex: 90 }} onClick={() => setShowUpcoming(false)} />}
<div style={{
position: 'absolute', top: 16, right: 24, bottom: 24,
width: 240, display: 'flex', flexDirection: 'column', gap: 10,
overflowY: 'auto',
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(30px)', WebkitBackdropFilter: 'blur(30px)',
borderRadius: 22, padding: '20px 16px',
border: '1px solid rgba(255,255,255,0.08)',
boxShadow: '0 16px 48px rgba(0,0,0,0.4)',
transform: showUpcoming ? 'translateX(0)' : 'translateX(calc(100% + 24px))',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
zIndex: 95, pointerEvents: showUpcoming ? 'auto' : 'none',
}}>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: 4 }}>Ближайшие</div>
{upcoming.length === 0 && !loading && (
<div style={{ fontSize: 13, color: 'var(--text-secondary)', padding: '20px 0', textAlign: 'center' }}>Нет событий</div>
)}
{upcoming.map(e => {
const d = new Date(e.start)
return (
<div key={e.id} onClick={() => setSelectedEvent(e)} style={{
borderRadius: 16, padding: '14px',
background: `${e.color}0c`, border: `1px solid ${e.color}1a`,
cursor: 'pointer', transition: 'all 0.25s ease',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<div style={{ width: 34, height: 34, borderRadius: 10, background: `${e.color}1a`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<span style={{ fontSize: 15, fontWeight: 700, color: e.color }}>{d.getDate()}</span>
</div>
<div>
<div style={{ fontSize: 10, color: e.color, fontWeight: 600 }}>{MONTHS[d.getMonth()].slice(0, 3)}</div>
<div style={{ fontSize: 10, color: 'var(--text-secondary)' }}>
{e.allDay ? 'Весь день' : formatTime(e.start)}
</div>
</div>
</div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{e.title}</div>
<div style={{ fontSize: 10, color: e.color, marginTop: 6, fontWeight: 500 }}>{e.ownerName}</div>
</div>
)
})}
</div>
{/* Modals */}
{selectedEvent && (
<EventDetailModal event={selectedEvent} onClose={() => setSelectedEvent(null)} onDelete={deleteEvent} />
)}
{dayPopover && (
<DayEventsModal
day={dayPopover.day} month={month} year={year} events={dayPopover.events}
onClose={() => setDayPopover(null)}
onSelect={e => { setDayPopover(null); setSelectedEvent(e) }}
/>
)}
{showAddModal && (
<AddEventModal
defaultDate={addDate}
onClose={() => setShowAddModal(false)}
onSaved={(newEvent) => { setEvents(prev => [...prev, newEvent]); setShowAddModal(false) }}
/>
)}
</div>
)
}