feat: wider calendar event strips, richer detail/add modals, day popover for multi-event days
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { ChevronLeft, ChevronRight, Plus, X, Clock, MapPin, Trash2, Eye, EyeOff } from 'lucide-react'
|
import { ChevronLeft, ChevronRight, Plus, X, Clock, MapPin, Trash2, Eye, EyeOff, CalendarDays, User, AlignLeft } from 'lucide-react'
|
||||||
|
|
||||||
interface CalendarEvent {
|
interface CalendarEvent {
|
||||||
id: string
|
id: string
|
||||||
@@ -18,6 +18,15 @@ interface CalendarEvent {
|
|||||||
const WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
|
const WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
|
||||||
const MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь']
|
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 }) {
|
function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string; onClose: () => void; onSaved: (e: any) => void }) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [date, setDate] = useState(defaultDate)
|
const [date, setDate] = useState(defaultDate)
|
||||||
@@ -27,10 +36,11 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string;
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const inputStyle = {
|
const inputStyle: React.CSSProperties = {
|
||||||
padding: '12px 16px', borderRadius: 14,
|
padding: '14px 18px', borderRadius: 14,
|
||||||
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)',
|
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)',
|
||||||
color: 'var(--text-primary)', fontSize: 14, outline: 'none', fontFamily: 'inherit',
|
color: 'var(--text-primary)', fontSize: 15, outline: 'none', fontFamily: 'inherit',
|
||||||
|
width: '100%',
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
@@ -46,30 +56,74 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(8px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={onClose}>
|
<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: 24, padding: 28, maxWidth: 360, width: '100%', boxShadow: '0 25px 60px rgba(0,0,0,0.5)' }} onClick={e => e.stopPropagation()}>
|
<div style={{
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
|
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(40px)',
|
||||||
<span style={{ fontSize: 18, fontWeight: 700, color: 'var(--text-primary)' }}>Новое событие</span>
|
border: '1px solid rgba(255,255,255,0.08)', borderRadius: 28,
|
||||||
<button onClick={onClose} style={{ color: 'var(--text-secondary)', padding: 4 }}><X size={18} /></button>
|
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>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<span style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)' }}>Новое событие</span>
|
||||||
<input value={title} onChange={e => setTitle(e.target.value)} placeholder="Название события" autoFocus style={inputStyle} />
|
</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} />
|
<input type="date" value={date} onChange={e => setDate(e.target.value)} style={inputStyle} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time */}
|
||||||
{!allDay && (
|
{!allDay && (
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<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 }} />
|
<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 }} />
|
<input type="time" value={endTime} onChange={e => setEndTime(e.target.value)} style={{ ...inputStyle, flex: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-secondary)', fontSize: 13, cursor: 'pointer' }}>
|
|
||||||
<input type="checkbox" checked={allDay} onChange={e => setAllDay(e.target.checked)} /> Весь день
|
{/* 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>
|
</label>
|
||||||
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
|
|
||||||
|
{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={{
|
<button onClick={save} disabled={saving} style={{
|
||||||
padding: '13px', borderRadius: 14,
|
padding: '15px', borderRadius: 16, marginTop: 4,
|
||||||
background: saving ? 'rgba(99,102,241,0.2)' : 'linear-gradient(135deg, rgba(99,102,241,0.4), rgba(139,92,246,0.3))',
|
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: 14, fontWeight: 600,
|
border: '1px solid rgba(129,140,248,0.3)', color: '#a5b4fc',
|
||||||
cursor: saving ? 'default' : 'pointer',
|
fontSize: 15, fontWeight: 700, cursor: saving ? 'default' : 'pointer',
|
||||||
|
transition: 'all 0.25s ease',
|
||||||
}}>
|
}}>
|
||||||
{saving ? 'Сохранение...' : 'Создать событие'}
|
{saving ? 'Сохранение...' : 'Создать событие'}
|
||||||
</button>
|
</button>
|
||||||
@@ -79,6 +133,213 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string;
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ————— 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() {
|
export default function CalendarTab() {
|
||||||
const [year, setYear] = useState(new Date().getFullYear())
|
const [year, setYear] = useState(new Date().getFullYear())
|
||||||
const [month, setMonth] = useState(new Date().getMonth())
|
const [month, setMonth] = useState(new Date().getMonth())
|
||||||
@@ -87,9 +348,8 @@ export default function CalendarTab() {
|
|||||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
|
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
const [addDate, setAddDate] = useState<string>('')
|
const [addDate, setAddDate] = useState<string>('')
|
||||||
const [deleting, setDeleting] = useState(false)
|
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
||||||
const [hiddenOwners, setHiddenOwners] = useState<Set<string>>(new Set())
|
const [hiddenOwners, setHiddenOwners] = useState<Set<string>>(new Set())
|
||||||
|
const [dayPopover, setDayPopover] = useState<{ day: number; events: CalendarEvent[] } | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -100,25 +360,21 @@ export default function CalendarTab() {
|
|||||||
}, [year, month])
|
}, [year, month])
|
||||||
|
|
||||||
const deleteEvent = async (event: CalendarEvent) => {
|
const deleteEvent = async (event: CalendarEvent) => {
|
||||||
setDeleting(true)
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/calendar?eventId=${event.id}`, { method: 'DELETE' })
|
const r = await fetch(`/api/calendar?eventId=${event.id}`, { method: 'DELETE' })
|
||||||
const d = await r.json()
|
const d = await r.json()
|
||||||
if (d.error) throw new Error(d.error)
|
if (d.error) throw new Error(d.error)
|
||||||
setEvents(prev => prev.filter(e => e.id !== event.id))
|
setEvents(prev => prev.filter(e => e.id !== event.id))
|
||||||
setSelectedEvent(null)
|
setSelectedEvent(null)
|
||||||
setConfirmDelete(false)
|
|
||||||
} catch (e: any) { alert(e.message || 'Ошибка удаления') }
|
} catch (e: any) { alert(e.message || 'Ошибка удаления') }
|
||||||
finally { setDeleting(false) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discover unique calendar owners from loaded events
|
|
||||||
const calendarOwners = useMemo(() => {
|
const calendarOwners = useMemo(() => {
|
||||||
const map = new Map<string, { owner: string; ownerName: string; color: string; count: number }>()
|
const map = new Map<string, { owner: string; ownerName: string; color: string; count: number }>()
|
||||||
events.forEach(e => {
|
events.forEach(e => {
|
||||||
const existing = map.get(e.owner)
|
const existing = map.get(e.owner)
|
||||||
if (existing) { existing.count++ }
|
if (existing) existing.count++
|
||||||
else { map.set(e.owner, { owner: e.owner, ownerName: e.ownerName, color: e.color, count: 1 }) }
|
else map.set(e.owner, { owner: e.owner, ownerName: e.ownerName, color: e.color, count: 1 })
|
||||||
})
|
})
|
||||||
return Array.from(map.values())
|
return Array.from(map.values())
|
||||||
}, [events])
|
}, [events])
|
||||||
@@ -162,6 +418,18 @@ export default function CalendarTab() {
|
|||||||
const nextMonth = () => { if (month === 11) { setMonth(0); 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 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 (
|
return (
|
||||||
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', padding: '16px 24px 24px', gap: 20 }}>
|
<div style={{ flex: 1, display: 'flex', overflow: 'hidden', padding: '16px 24px 24px', gap: 20 }}>
|
||||||
{/* Main calendar grid */}
|
{/* Main calendar grid */}
|
||||||
@@ -181,54 +449,40 @@ export default function CalendarTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
{/* Calendar owner filters */}
|
|
||||||
{calendarOwners.map(cal => {
|
{calendarOwners.map(cal => {
|
||||||
const isHidden = hiddenOwners.has(cal.owner)
|
const isHidden = hiddenOwners.has(cal.owner)
|
||||||
return (
|
return (
|
||||||
<button
|
<button key={cal.owner} onClick={() => toggleOwner(cal.owner)} style={{
|
||||||
key={cal.owner}
|
|
||||||
onClick={() => toggleOwner(cal.owner)}
|
|
||||||
style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '7px 14px', borderRadius: 12,
|
padding: '7px 14px', borderRadius: 12,
|
||||||
background: isHidden ? 'rgba(255,255,255,0.02)' : `${cal.color}15`,
|
background: isHidden ? 'rgba(255,255,255,0.02)' : `${cal.color}15`,
|
||||||
border: `1px solid ${isHidden ? 'rgba(255,255,255,0.06)' : cal.color + '30'}`,
|
border: `1px solid ${isHidden ? 'rgba(255,255,255,0.06)' : cal.color + '30'}`,
|
||||||
color: isHidden ? 'var(--text-tertiary)' : cal.color,
|
color: isHidden ? 'var(--text-tertiary)' : cal.color,
|
||||||
fontSize: 12, fontWeight: 600,
|
fontSize: 12, fontWeight: 600, transition: 'all 0.25s ease',
|
||||||
transition: 'all 0.25s ease',
|
|
||||||
opacity: isHidden ? 0.5 : 1,
|
opacity: isHidden ? 0.5 : 1,
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{isHidden ? <EyeOff size={13} /> : <Eye size={13} />}
|
{isHidden ? <EyeOff size={13} /> : <Eye size={13} />}
|
||||||
{cal.ownerName}
|
{cal.ownerName}
|
||||||
<span style={{
|
<span style={{ fontSize: 10, padding: '1px 6px', borderRadius: 6, background: isHidden ? 'rgba(255,255,255,0.05)' : `${cal.color}20` }}>
|
||||||
fontSize: 10, padding: '1px 6px', borderRadius: 6,
|
|
||||||
background: isHidden ? 'rgba(255,255,255,0.05)' : `${cal.color}20`,
|
|
||||||
}}>
|
|
||||||
{cal.count}
|
{cal.count}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
<button onClick={() => { setAddDate(today.toISOString().split('T')[0]); setShowAddModal(true) }} style={{
|
||||||
<button
|
|
||||||
onClick={() => { setAddDate(today.toISOString().split('T')[0]); setShowAddModal(true) }}
|
|
||||||
style={{
|
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
padding: '8px 18px', borderRadius: 14,
|
padding: '8px 18px', borderRadius: 14,
|
||||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))',
|
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)',
|
border: '1px solid rgba(129,140,248,0.25)',
|
||||||
color: '#a5b4fc', fontSize: 13, fontWeight: 600,
|
color: '#a5b4fc', fontSize: 13, fontWeight: 600,
|
||||||
}}
|
}}>
|
||||||
>
|
<Plus size={15} /> Событие
|
||||||
<Plus size={15} />
|
|
||||||
Событие
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Weekday headers */}
|
{/* Weekday headers */}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', marginBottom: 6 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', marginBottom: 4 }}>
|
||||||
{WEEKDAYS.map((d, i) => (
|
{WEEKDAYS.map((d, i) => (
|
||||||
<div key={d} style={{
|
<div key={d} style={{
|
||||||
textAlign: 'center', fontSize: 11, fontWeight: 600,
|
textAlign: 'center', fontSize: 11, fontWeight: 600,
|
||||||
@@ -249,39 +503,52 @@ export default function CalendarTab() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => {
|
onClick={() => handleDayClick(day)}
|
||||||
if (dayEvents.length === 1) setSelectedEvent(dayEvents[0])
|
|
||||||
else if (dayEvents.length === 0) {
|
|
||||||
setAddDate(`${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`)
|
|
||||||
setShowAddModal(true)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 12, padding: '5px 4px',
|
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)',
|
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',
|
border: isToday ? '1px solid rgba(129,140,248,0.3)' : '1px solid transparent',
|
||||||
cursor: 'pointer', minHeight: 56,
|
cursor: 'pointer', minHeight: 70,
|
||||||
display: 'flex', flexDirection: 'column', gap: 2,
|
display: 'flex', flexDirection: 'column', gap: 3,
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 12, fontWeight: isToday ? 700 : 500,
|
fontSize: 12, fontWeight: isToday ? 700 : 500,
|
||||||
color: isToday ? '#a5b4fc' : isWeekend ? 'rgba(248,113,113,0.6)' : 'var(--text-secondary)',
|
color: isToday ? '#a5b4fc' : isWeekend ? 'rgba(248,113,113,0.6)' : 'var(--text-secondary)',
|
||||||
textAlign: 'right', paddingRight: 4,
|
textAlign: 'right', paddingRight: 3, lineHeight: 1,
|
||||||
}}>{day}</span>
|
}}>{day}</span>
|
||||||
{dayEvents.slice(0, 2).map(e => (
|
{dayEvents.slice(0, 3).map(e => (
|
||||||
<div key={e.id} onClick={ev => { ev.stopPropagation(); setSelectedEvent(e) }} style={{
|
<div
|
||||||
fontSize: 10, fontWeight: 600,
|
key={e.id}
|
||||||
background: e.color + '1a', border: `1px solid ${e.color}30`,
|
onClick={ev => { ev.stopPropagation(); setSelectedEvent(e) }}
|
||||||
color: e.color, borderRadius: 6, padding: '2px 5px',
|
style={{
|
||||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer',
|
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}
|
{e.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{dayEvents.length > 2 && (
|
{dayEvents.length > 3 && (
|
||||||
<div style={{ fontSize: 9, color: 'var(--text-secondary)', textAlign: 'center' }}>+{dayEvents.length - 2}</div>
|
<div style={{ fontSize: 10, color: 'var(--text-secondary)', textAlign: 'center', fontWeight: 500 }}>
|
||||||
|
+{dayEvents.length - 3}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -291,7 +558,7 @@ export default function CalendarTab() {
|
|||||||
|
|
||||||
{/* Right panel */}
|
{/* Right panel */}
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 220, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 10,
|
width: 230, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 10,
|
||||||
overflowY: 'auto', background: 'rgba(255,255,255,0.02)', borderRadius: 22,
|
overflowY: 'auto', background: 'rgba(255,255,255,0.02)', borderRadius: 22,
|
||||||
padding: '20px 16px', border: '1px solid rgba(255,255,255,0.04)',
|
padding: '20px 16px', border: '1px solid rgba(255,255,255,0.04)',
|
||||||
}}>
|
}}>
|
||||||
@@ -308,86 +575,34 @@ export default function CalendarTab() {
|
|||||||
cursor: 'pointer', transition: 'all 0.25s ease',
|
cursor: 'pointer', transition: 'all 0.25s ease',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||||
<div style={{ width: 32, height: 32, borderRadius: 10, background: `${e.color}1a`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
<div style={{ width: 34, height: 34, borderRadius: 10, background: `${e.color}1a`, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||||
<span style={{ fontSize: 14, fontWeight: 700, color: e.color }}>{d.getDate()}</span>
|
<span style={{ fontSize: 15, fontWeight: 700, color: e.color }}>{d.getDate()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div style={{ fontSize: 10, color: e.color, fontWeight: 600 }}>{MONTHS[d.getMonth()].slice(0, 3)}</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>
|
||||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{e.title}</div>
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{e.title}</div>
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 4 }}>
|
<div style={{ fontSize: 10, color: e.color, marginTop: 6, fontWeight: 500 }}>{e.ownerName}</div>
|
||||||
{e.allDay ? 'Весь день' : d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 10, color: e.color, marginTop: 4, fontWeight: 500 }}>{e.ownerName}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Event detail modal */}
|
{/* Modals */}
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(8px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={() => { setSelectedEvent(null); setConfirmDelete(false) }}>
|
<EventDetailModal event={selectedEvent} onClose={() => setSelectedEvent(null)} onDelete={deleteEvent} />
|
||||||
<div style={{
|
|
||||||
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(40px)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.08)', borderRadius: 24, padding: 28,
|
|
||||||
maxWidth: 380, width: '100%', boxShadow: '0 25px 60px rgba(0,0,0,0.5)',
|
|
||||||
}} onClick={e => e.stopPropagation()}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 20 }}>
|
|
||||||
<div style={{ display: 'flex', gap: 14 }}>
|
|
||||||
<div style={{ width: 4, borderRadius: 3, background: selectedEvent.color, alignSelf: 'stretch', minHeight: 44, flexShrink: 0 }} />
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 700, color: 'var(--text-primary)' }}>{selectedEvent.title}</div>
|
|
||||||
<div style={{ fontSize: 13, color: selectedEvent.color, fontWeight: 500, marginTop: 3 }}>{selectedEvent.ownerName}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button onClick={() => { setSelectedEvent(null); setConfirmDelete(false) }} style={{ color: 'var(--text-secondary)', padding: 4 }}><X size={18} /></button>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', borderRadius: 12, background: 'rgba(255,255,255,0.03)', color: 'var(--text-secondary)', fontSize: 13 }}>
|
|
||||||
<Clock size={15} />
|
|
||||||
{selectedEvent.allDay ? 'Весь день' : `${new Date(selectedEvent.start).toLocaleString('ru-RU', { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' })} — ${new Date(selectedEvent.end).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}`}
|
|
||||||
</div>
|
|
||||||
{selectedEvent.location && (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', borderRadius: 12, background: 'rgba(255,255,255,0.03)', color: 'var(--text-secondary)', fontSize: 13 }}>
|
|
||||||
<MapPin size={15} /> {selectedEvent.location}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{selectedEvent.description && (
|
|
||||||
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 4, lineHeight: 1.6 }}>{selectedEvent.description}</div>
|
{dayPopover && (
|
||||||
)}
|
<DayEventsModal
|
||||||
<div style={{ marginTop: 8, borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: 14 }}>
|
day={dayPopover.day} month={month} year={year} events={dayPopover.events}
|
||||||
{!confirmDelete ? (
|
onClose={() => setDayPopover(null)}
|
||||||
<button onClick={() => setConfirmDelete(true)} style={{
|
onSelect={e => { setDayPopover(null); setSelectedEvent(e) }}
|
||||||
display: 'flex', alignItems: 'center', gap: 6,
|
/>
|
||||||
padding: '10px 14px', borderRadius: 12,
|
|
||||||
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)',
|
|
||||||
color: '#f87171', fontSize: 13, fontWeight: 600,
|
|
||||||
width: '100%', justifyContent: 'center',
|
|
||||||
}}>
|
|
||||||
<Trash2 size={14} /> Удалить событие
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
|
||||||
<button onClick={() => deleteEvent(selectedEvent)} disabled={deleting} style={{
|
|
||||||
flex: 1, padding: '10px 14px', borderRadius: 12,
|
|
||||||
background: deleting ? 'rgba(239,68,68,0.1)' : 'rgba(239,68,68,0.2)',
|
|
||||||
border: '1px solid rgba(239,68,68,0.35)',
|
|
||||||
color: '#f87171', fontSize: 13, fontWeight: 600,
|
|
||||||
}}>
|
|
||||||
{deleting ? 'Удаление...' : 'Да, удалить'}
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setConfirmDelete(false)} style={{
|
|
||||||
flex: 1, padding: '10px 14px', borderRadius: 12,
|
|
||||||
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
color: 'var(--text-secondary)', fontSize: 13, fontWeight: 600,
|
|
||||||
}}>
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
|
|||||||
Reference in New Issue
Block a user