feat: event editing, light/dark theme, device animations, 7-day forecast
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
Cosmo
2026-04-22 19:56:38 +00:00
parent b797d0d660
commit 690db4c6cf
7 changed files with 394 additions and 126 deletions

View File

@@ -155,6 +155,54 @@ export async function POST(req: Request) {
}
}
export async function PUT(req: Request) {
const body = await req.json()
const { eventId, title, date, startTime, endTime, allDay, calendarId } = body
if (!eventId) {
return NextResponse.json({ error: 'eventId is required' }, { status: 400 })
}
const auth = getAuth(false)
if (!auth) return NextResponse.json({ error: 'not_configured' }, { status: 500 })
const targetCalendarId = calendarId || process.env.DANIIL_CALENDAR_ID || 'daniilklimov25@gmail.com'
const calendarClient = google.calendar({ version: 'v3', auth: auth as any })
let start: any, end: any
if (allDay) {
start = { date }
end = { date }
} else {
start = { dateTime: , timeZone: 'Europe/Moscow' }
end = { dateTime: , timeZone: 'Europe/Moscow' }
}
try {
const res = await calendarClient.events.patch({
calendarId: targetCalendarId,
eventId,
requestBody: {
summary: title,
start,
end,
},
})
const e = res.data
return NextResponse.json({
event: {
id: e.id,
title: e.summary || title,
start: e.start?.dateTime || e.start?.date,
end: e.end?.dateTime || e.end?.date,
allDay: !e.start?.dateTime,
}
})
} catch (err: any) {
return NextResponse.json({ error: err.message || 'Failed to update event' }, { status: 500 })
}
}
export async function DELETE(req: Request) {
const { searchParams } = new URL(req.url)
const eventId = searchParams.get('eventId')

View File

@@ -51,7 +51,7 @@ export async function GET(req: Request) {
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
daily: "weather_code,temperature_2m_max,temperature_2m_min",
timezone: "Europe/Moscow",
forecast_days: "3",
forecast_days: "7",
});
const res = await fetch(url, {

View File

@@ -156,3 +156,66 @@ button:focus-visible {
.animate-slide-up {
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
/* ————— Light theme ————— */
.light {
--bg: #f5f5fa;
--bg-secondary: #eeeef4;
--sidebar-bg: rgba(255, 255, 255, 0.8);
--card-bg: rgba(255, 255, 255, 0.7);
--card-bg-hover: rgba(255, 255, 255, 0.85);
--card-border: rgba(0, 0, 0, 0.06);
--card-border-hover: rgba(0, 0, 0, 0.1);
--text-primary: rgba(0, 0, 0, 0.88);
--text-secondary: rgba(0, 0, 0, 0.45);
--text-tertiary: rgba(0, 0, 0, 0.2);
--accent: #6366f1;
--accent-secondary: #0891b2;
--accent-glow: rgba(99, 102, 241, 0.12);
--glass: rgba(255, 255, 255, 0.5);
--glass-border: rgba(0, 0, 0, 0.06);
--on-color: #6366f1;
--off-color: rgba(0, 0, 0, 0.12);
}
.light .bg-ambient::before {
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
}
.light .bg-ambient::after {
background: radial-gradient(circle, rgba(139, 92, 246, 0.04) 0%, transparent 70%);
}
.light ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
}
.light ::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.2);
}
/* ————— Device animations ————— */
@keyframes fan-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes light-pulse {
0%, 100% { filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.3)); }
50% { filter: drop-shadow(0 0 12px rgba(251, 191, 36, 0.6)); }
}
@keyframes device-breathe {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.fan-spinning {
animation: fan-spin 2s linear infinite;
}
.light-on-pulse {
animation: light-pulse 3s ease-in-out infinite;
}
.device-active-breathe {
animation: device-breathe 3s ease-in-out infinite;
}

View File

@@ -391,7 +391,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
}
// ————— Settings Tab —————
function SettingsTab({ city, onCityChange, onLogout }: { city: string; onCityChange: (id: string) => void; onLogout: () => void }) {
function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { city: string; onCityChange: (id: string) => void; onLogout: () => void; theme: string; onThemeChange: (t: string) => void }) {
const [showPinChange, setShowPinChange] = useState(false)
const [oldPin, setOldPin] = useState('')
const [newPin, setNewPin] = useState('')
@@ -431,7 +431,7 @@ function SettingsTab({ city, onCityChange, onLogout }: { city: string; onCityCha
<h2 style={{ fontSize: 24, fontWeight: 800, color: 'var(--text-primary)', margin: '0 0 8px', letterSpacing: '-0.5px' }}>Настройки</h2>
{/* City selector */}
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
<MapPin size={18} color="#818cf8" />
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Город</span>
@@ -457,8 +457,35 @@ function SettingsTab({ city, onCityChange, onLogout }: { city: string; onCityCha
</div>
</div>
{/* Theme */}
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 18 }}>{theme === 'dark' ? '🌙' : '☀️'}</span>
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Тема</span>
</div>
<div style={{ display: 'flex', gap: 6 }}>
{[
{ id: 'dark', label: 'Тёмная' },
{ id: 'light', label: 'Светлая' },
].map(t => (
<button key={t.id} onClick={() => onThemeChange(t.id)} style={{
padding: '8px 16px', borderRadius: 12,
background: theme === t.id ? 'rgba(99,102,241,0.12)' : 'rgba(255,255,255,0.02)',
border: theme === t.id ? '1px solid rgba(129,140,248,0.25)' : '1px solid var(--card-border)',
color: theme === t.id ? '#a5b4fc' : 'var(--text-secondary)',
fontSize: 13, fontWeight: theme === t.id ? 600 : 500,
transition: 'all 0.25s ease',
}}>
{t.label}
</button>
))}
</div>
</div>
</div>
{/* PIN change */}
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 22, padding: '22px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<KeyRound size={18} color="#818cf8" />
@@ -551,8 +578,18 @@ function HomePageInner() {
return 'spb'
})
const [screensaverActive, setScreensaverActive] = useState(false)
const [theme, setTheme] = useState(() => {
if (typeof window !== 'undefined') return localStorage.getItem('tablet-theme') || 'dark'
return 'dark'
})
const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
// Theme
useEffect(() => {
document.documentElement.className = theme
localStorage.setItem('tablet-theme', theme)
}, [theme])
// Auth check
useEffect(() => {
fetch('/api/auth')
@@ -690,7 +727,7 @@ function HomePageInner() {
{tab === 'settings' && (
<motion.div key="settings" variants={tabVariants} initial="enter" animate="center" exit="exit" transition={{ duration: 0.2 }} style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<SettingsTab city={city} onCityChange={handleCityChange} onLogout={handleLogout} />
<SettingsTab city={city} onCityChange={handleCityChange} onLogout={handleLogout} theme={theme} onThemeChange={setTheme} />
</motion.div>
)}
</AnimatePresence>

View File

@@ -255,13 +255,28 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string;
}
// ————— Event Detail Modal —————
function EventDetailModal({ event, onClose, onDelete }: {
function EventDetailModal({ event, onClose, onDelete, onUpdate }: {
event: CalendarEvent
onClose: () => void
onDelete: (e: CalendarEvent) => Promise<void>
onUpdate: (old: CalendarEvent, updated: Partial<CalendarEvent>) => Promise<void>
}) {
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)
@@ -269,151 +284,254 @@ function EventDetailModal({ event, onClose, onDelete }: {
setDeleting(false)
}
const startDate = new Date(event.start)
const endDate = new Date(event.end)
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 (
<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',
background: 'rgba(16,16,30,0.97)', backdropFilter: 'blur(40px)',
border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28,
width: 480, maxWidth: '95vw', overflow: 'hidden',
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
}} onClick={e => e.stopPropagation()}>
{/* Colored header band */}
{/* Colored header */}
<div style={{
background: `linear-gradient(135deg, ${event.color}25, ${event.color}10)`,
borderBottom: `1px solid ${event.color}20`,
padding: '28px 32px 24px',
background: `linear-gradient(135deg, ${event.color}20, ${event.color}08)`,
borderBottom: `1px solid ${event.color}15`,
padding: '24px 28px 20px',
}}>
<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,
}}>
{editing ? (
<input
value={editTitle} onChange={e => 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',
}}
/>
) : (
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1.3 }}>
{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 style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: event.color, boxShadow: `0 0 8px ${event.color}60` }} />
<span style={{ fontSize: 13, 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,
<div style={{ display: 'flex', gap: 6, marginLeft: 12, flexShrink: 0 }}>
{!editing && (
<button onClick={() => setEditing(true)} style={{
padding: '8px 14px', borderRadius: 10,
background: 'rgba(255,255,255,0.06)',
color: 'var(--text-secondary)', fontSize: 12, fontWeight: 600,
}}>
<X size={18} />
Изменить
</button>
)}
<button onClick={onClose} style={{
width: 32, height: 32, borderRadius: 10,
background: 'rgba(255,255,255,0.06)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-secondary)',
}}>
<X size={16} />
</button>
</div>
</div>
</div>
{/* Content */}
<div style={{ padding: '24px 32px 28px', display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ padding: '20px 28px 24px', display: 'flex', flexDirection: 'column', gap: 14 }}>
{/* 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>
{editing ? (
<>
{/* Date */}
<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 style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>Дата</div>
<input type="date" value={editDate} onChange={e => 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',
}} />
</div>
{/* Location */}
{event.location && (
{/* All day toggle */}
<button onClick={() => setEditAllDay(v => !v)} style={{
width: '100%', padding: '12px 16px', borderRadius: 12,
background: editAllDay ? `${event.color}10` : 'rgba(255,255,255,0.025)',
border: `1px solid ${editAllDay ? event.color + '25' : 'rgba(255,255,255,0.06)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<span style={{ fontSize: 14, fontWeight: 500, color: editAllDay ? event.color : 'var(--text-secondary)' }}>Весь день</span>
<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)',
width: 40, height: 22, borderRadius: 11,
background: editAllDay ? event.color : 'rgba(255,255,255,0.1)',
position: 'relative', transition: 'background 0.2s ease',
}}>
<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" />
width: 18, height: 18, borderRadius: '50%', background: '#fff',
position: 'absolute', top: 2, left: editAllDay ? 20 : 2,
transition: 'left 0.2s ease', boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
}} />
</div>
<div style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 500 }}>{event.location}</div>
</div>
)}
</button>
{/* Description */}
{event.description && (
{/* Time */}
{!editAllDay && (
<div>
<div style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>Время</div>
<div style={{
padding: '16px 18px', borderRadius: 16,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
display: 'flex', alignItems: 'center',
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)',
borderRadius: 12, overflow: 'hidden',
}}>
<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>
<input type="time" value={editStartTime} onChange={e => 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',
}} />
<div style={{
fontSize: 14, color: 'var(--text-primary)', lineHeight: 1.7,
whiteSpace: 'pre-wrap', fontWeight: 400,
}}>
{event.description}
padding: '0 10px', color: 'var(--text-tertiary)', fontSize: 12,
borderLeft: '1px solid rgba(255,255,255,0.06)',
borderRight: '1px solid rgba(255,255,255,0.06)',
background: 'rgba(255,255,255,0.02)',
height: '100%', display: 'flex', alignItems: 'center',
}}>до</div>
<input type="time" value={editEndTime} onChange={e => 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',
}} />
</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',
{saveError && (
<div style={{ color: '#f87171', fontSize: 13, padding: '8px 12px', borderRadius: 10, background: 'rgba(239,68,68,0.08)' }}>{saveError}</div>
)}
{/* Save / Cancel */}
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
<button onClick={handleSave} disabled={saving} style={{
flex: 1, padding: '13px', borderRadius: 14,
background: `linear-gradient(135deg, ${event.color}45, ${event.color}30)`,
border: `1px solid ${event.color}35`,
color: '#c7d2fe', fontSize: 14, fontWeight: 700,
}}>
<Trash2 size={16} /> Удалить событие
{saving ? 'Сохранение...' : 'Сохранить'}
</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,
<button onClick={() => { setEditing(false); setEditTitle(event.title) }} style={{
padding: '13px 20px', 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>
</>
) : (
<>
{/* View mode */}
<div style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', borderRadius: 14,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
}}>
<CalendarDays size={18} color={event.color} />
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', textTransform: 'capitalize' }}>
{formatFullDate(event.start)}
</div>
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3 }}>
{event.allDay ? 'Весь день' : `${formatTime(event.start)}${formatTime(event.end)}`}
</div>
</div>
</div>
{event.location && (
<div style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', borderRadius: 14,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
}}>
<MapPin size={18} color="#fb923c" />
<div style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 500 }}>{event.location}</div>
</div>
)}
{event.description && (
<div style={{
padding: '14px 16px', borderRadius: 14,
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: 8 }}>
<AlignLeft size={14} color="var(--text-secondary)" />
<span style={{ fontSize: 11, 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' }}>{event.description}</div>
</div>
)}
{/* Delete */}
<div style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: 14, marginTop: 4 }}>
{!confirmDelete ? (
<button onClick={() => setConfirmDelete(true)} style={{
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
padding: '12px', borderRadius: 14, width: '100%',
background: 'rgba(239,68,68,0.06)', border: '1px solid rgba(239,68,68,0.15)',
color: '#f87171', fontSize: 13, fontWeight: 600,
}}>
<Trash2 size={15} /> Удалить
</button>
) : (
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={handleDelete} disabled={deleting} style={{
flex: 1, padding: '12px', borderRadius: 14,
background: deleting ? 'rgba(239,68,68,0.06)' : 'rgba(239,68,68,0.12)',
border: '1px solid rgba(239,68,68,0.25)',
color: '#f87171', fontSize: 13, fontWeight: 600,
}}>
{deleting ? 'Удаление...' : 'Да, удалить'}
</button>
<button onClick={() => setConfirmDelete(false)} style={{
flex: 1, padding: '12px', borderRadius: 14,
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>
)
}
// ————— Day Events Popover —————
function DayEventsModal({ day, month, year, events, onClose, onSelect }: {
day: number; month: number; year: number
@@ -734,7 +852,7 @@ export default function CalendarTab() {
{/* Modals */}
{selectedEvent && (
<EventDetailModal event={selectedEvent} onClose={() => setSelectedEvent(null)} onDelete={deleteEvent} />
<EventDetailModal event={selectedEvent} onClose={() => setSelectedEvent(null)} onDelete={deleteEvent} onUpdate={updateEvent} />
)}
{dayPopover && (

View File

@@ -111,7 +111,9 @@ export default function DeviceCard({
{/* Top: icon + toggle */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', position: 'relative', zIndex: 1 }}>
<div style={{
<div
className={isOn ? (id.includes('air_purifier') ? 'fan-spinning' : id.includes('light') ? 'light-on-pulse' : 'device-active-breathe') : ''}
style={{
width: 48, height: 48, borderRadius: 16,
background: isOn ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.05)',
display: 'flex', alignItems: 'center', justifyContent: 'center',

View File

@@ -255,7 +255,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase',
letterSpacing: '0.1em', fontWeight: 600, marginBottom: 14,
}}>
Прогноз на ближайшие дни
Прогноз на неделю
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{weather.forecast.map(day => {