Files
smart-home-tablet/components/CalendarTab.tsx
Cosmo b797d0d660
Some checks failed
Deploy / deploy (push) Failing after 2m9s
fix: redesign add-event modal — vertical layout, toggle switch, unified time picker
2026-04-22 19:50:35 +00:00

758 lines
34 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 —————
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 (
<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(16,16,30,0.97)', backdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28,
width: 420, maxWidth: '95vw', overflow: 'hidden',
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
}} onClick={e => e.stopPropagation()}>
{/* Header */}
<div style={{
padding: '24px 28px 20px',
borderBottom: '1px solid rgba(255,255,255,0.05)',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<span style={{ fontSize: 18, fontWeight: 700, color: 'var(--text-primary)' }}>
Новое событие
</span>
<button onClick={onClose} style={{
width: 32, height: 32, borderRadius: 10,
background: 'rgba(255,255,255,0.05)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-secondary)',
}}>
<X size={16} />
</button>
</div>
{/* Body */}
<div style={{ padding: '20px 28px 28px' }}>
{/* Calendar selector */}
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
{CALENDAR_OPTIONS.map(cal => {
const sel = owner === cal.owner
return (
<button key={cal.owner} onClick={() => setOwner(cal.owner)} style={{
flex: 1, padding: '11px 0', borderRadius: 12,
background: sel ? `${cal.color}15` : 'rgba(255,255,255,0.03)',
border: `1.5px solid ${sel ? cal.color + '40' : 'rgba(255,255,255,0.06)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
transition: 'all 0.2s ease',
}}>
<div style={{
width: 10, height: 10, borderRadius: '50%',
background: sel ? cal.color : 'rgba(255,255,255,0.15)',
boxShadow: sel ? `0 0 8px ${cal.color}50` : 'none',
transition: 'all 0.2s ease',
}} />
<span style={{
fontSize: 14, fontWeight: sel ? 600 : 400,
color: sel ? cal.color : 'var(--text-secondary)',
}}>{cal.name}</span>
</button>
)
})}
</div>
{/* Title */}
<input
value={title} onChange={e => 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 */}
<div style={{ marginBottom: 16 }}>
<div style={{
fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.08em',
marginBottom: 8,
}}>Дата</div>
<input
type="date" value={date} onChange={e => 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',
}}
/>
</div>
{/* All day toggle */}
<button onClick={() => setAllDay(v => !v)} style={{
width: '100%', padding: '13px 18px', borderRadius: 14,
background: allDay ? `${selectedCal.color}10` : 'rgba(255,255,255,0.025)',
border: `1px solid ${allDay ? selectedCal.color + '25' : 'rgba(255,255,255,0.06)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
marginBottom: 16, transition: 'all 0.2s ease',
}}>
<span style={{
fontSize: 14, fontWeight: 500,
color: allDay ? selectedCal.color : 'var(--text-secondary)',
}}>Весь день</span>
<div style={{
width: 40, height: 22, borderRadius: 11,
background: allDay ? selectedCal.color : 'rgba(255,255,255,0.1)',
position: 'relative', transition: 'background 0.2s ease',
}}>
<div style={{
width: 18, height: 18, borderRadius: '50%',
background: '#fff', position: 'absolute', top: 2,
left: allDay ? 20 : 2,
transition: 'left 0.2s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
}} />
</div>
</button>
{/* Time pickers */}
{!allDay && (
<div style={{ marginBottom: 16 }}>
<div style={{
fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.08em',
marginBottom: 8,
}}>Время</div>
<div style={{
display: 'flex', alignItems: 'center',
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.07)',
borderRadius: 14, overflow: 'hidden',
}}>
<input
type="time" value={startTime} onChange={e => 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',
}}
/>
<div style={{
padding: '0 12px', color: 'var(--text-tertiary)',
fontSize: 13, fontWeight: 500, flexShrink: 0,
borderLeft: '1px solid rgba(255,255,255,0.06)',
borderRight: '1px solid rgba(255,255,255,0.06)',
height: '100%', display: 'flex', alignItems: 'center',
background: 'rgba(255,255,255,0.02)',
}}>до</div>
<input
type="time" value={endTime} onChange={e => 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',
}}
/>
</div>
</div>
)}
{/* Error */}
{error && (
<div style={{
padding: '10px 14px', borderRadius: 12, marginBottom: 16,
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.12)',
color: '#f87171', fontSize: 13, fontWeight: 500,
}}>{error}</div>
)}
{/* Submit */}
<button onClick={save} disabled={saving} style={{
width: '100%', padding: '15px', borderRadius: 14,
background: saving
? 'rgba(255,255,255,0.04)'
: `linear-gradient(135deg, ${selectedCal.color}45, ${selectedCal.color}30)`,
border: `1px solid ${selectedCal.color}35`,
color: selectedCal.color === '#ec4899' ? '#f9a8d4' : '#c7d2fe',
fontSize: 15, fontWeight: 700,
cursor: saving ? 'default' : 'pointer',
transition: 'all 0.25s ease',
boxShadow: saving ? 'none' : `0 4px 16px ${selectedCal.color}18`,
}}>
{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>
)
}