feat: event editing, light/dark theme, device animations, 7-day forecast
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -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) {
|
export async function DELETE(req: Request) {
|
||||||
const { searchParams } = new URL(req.url)
|
const { searchParams } = new URL(req.url)
|
||||||
const eventId = searchParams.get('eventId')
|
const eventId = searchParams.get('eventId')
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export async function GET(req: Request) {
|
|||||||
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
||||||
daily: "weather_code,temperature_2m_max,temperature_2m_min",
|
daily: "weather_code,temperature_2m_max,temperature_2m_min",
|
||||||
timezone: "Europe/Moscow",
|
timezone: "Europe/Moscow",
|
||||||
forecast_days: "3",
|
forecast_days: "7",
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
|
|||||||
@@ -156,3 +156,66 @@ button:focus-visible {
|
|||||||
.animate-slide-up {
|
.animate-slide-up {
|
||||||
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
45
app/page.tsx
45
app/page.tsx
@@ -391,7 +391,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ————— Settings Tab —————
|
// ————— 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 [showPinChange, setShowPinChange] = useState(false)
|
||||||
const [oldPin, setOldPin] = useState('')
|
const [oldPin, setOldPin] = useState('')
|
||||||
const [newPin, setNewPin] = 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>
|
<h2 style={{ fontSize: 24, fontWeight: 800, color: 'var(--text-primary)', margin: '0 0 8px', letterSpacing: '-0.5px' }}>Настройки</h2>
|
||||||
|
|
||||||
{/* City selector */}
|
{/* 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 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
|
||||||
<MapPin size={18} color="#818cf8" />
|
<MapPin size={18} color="#818cf8" />
|
||||||
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Город</span>
|
<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>
|
||||||
</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 */}
|
{/* 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', justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<KeyRound size={18} color="#818cf8" />
|
<KeyRound size={18} color="#818cf8" />
|
||||||
@@ -551,8 +578,18 @@ function HomePageInner() {
|
|||||||
return 'spb'
|
return 'spb'
|
||||||
})
|
})
|
||||||
const [screensaverActive, setScreensaverActive] = useState(false)
|
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)
|
const idleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.className = theme
|
||||||
|
localStorage.setItem('tablet-theme', theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
// Auth check
|
// Auth check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/auth')
|
fetch('/api/auth')
|
||||||
@@ -690,7 +727,7 @@ function HomePageInner() {
|
|||||||
|
|
||||||
{tab === 'settings' && (
|
{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' }}>
|
<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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@@ -255,13 +255,28 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ————— Event Detail Modal —————
|
// ————— Event Detail Modal —————
|
||||||
function EventDetailModal({ event, onClose, onDelete }: {
|
function EventDetailModal({ event, onClose, onDelete, onUpdate }: {
|
||||||
event: CalendarEvent
|
event: CalendarEvent
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onDelete: (e: CalendarEvent) => Promise<void>
|
onDelete: (e: CalendarEvent) => Promise<void>
|
||||||
|
onUpdate: (old: CalendarEvent, updated: Partial<CalendarEvent>) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||||
const [deleting, setDeleting] = 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 () => {
|
const handleDelete = async () => {
|
||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
@@ -269,151 +284,254 @@ function EventDetailModal({ event, onClose, onDelete }: {
|
|||||||
setDeleting(false)
|
setDeleting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = new Date(event.start)
|
const handleSave = async () => {
|
||||||
const endDate = new Date(event.end)
|
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 (
|
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={{ 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={{
|
<div style={{
|
||||||
background: 'rgba(18,18,35,0.95)', backdropFilter: 'blur(40px)',
|
background: 'rgba(16,16,30,0.97)', backdropFilter: 'blur(40px)',
|
||||||
border: '1px solid rgba(255,255,255,0.08)', borderRadius: 28,
|
border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28,
|
||||||
padding: 0, width: 480, maxWidth: '95vw',
|
width: 480, maxWidth: '95vw', overflow: 'hidden',
|
||||||
boxShadow: '0 25px 80px rgba(0,0,0,0.6)', overflow: 'hidden',
|
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
|
||||||
}} onClick={e => e.stopPropagation()}>
|
}} onClick={e => e.stopPropagation()}>
|
||||||
|
|
||||||
{/* Colored header band */}
|
{/* Colored header */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: `linear-gradient(135deg, ${event.color}25, ${event.color}10)`,
|
background: `linear-gradient(135deg, ${event.color}20, ${event.color}08)`,
|
||||||
borderBottom: `1px solid ${event.color}20`,
|
borderBottom: `1px solid ${event.color}15`,
|
||||||
padding: '28px 32px 24px',
|
padding: '24px 28px 20px',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{
|
{editing ? (
|
||||||
fontSize: 22, fontWeight: 700, color: 'var(--text-primary)',
|
<input
|
||||||
lineHeight: 1.3, marginBottom: 8,
|
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}
|
{event.title}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
)}
|
||||||
<div style={{
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 }}>
|
||||||
width: 8, height: 8, borderRadius: '50%', background: event.color, flexShrink: 0,
|
<div style={{ width: 8, height: 8, borderRadius: '50%', background: event.color, boxShadow: `0 0 8px ${event.color}60` }} />
|
||||||
boxShadow: `0 0 8px ${event.color}60`,
|
<span style={{ fontSize: 13, color: event.color, fontWeight: 600 }}>{event.ownerName}</span>
|
||||||
}} />
|
|
||||||
<span style={{ fontSize: 14, color: event.color, fontWeight: 600 }}>{event.ownerName}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} style={{
|
<div style={{ display: 'flex', gap: 6, marginLeft: 12, flexShrink: 0 }}>
|
||||||
color: 'var(--text-secondary)', padding: 8, borderRadius: 12,
|
{!editing && (
|
||||||
background: 'rgba(255,255,255,0.06)', flexShrink: 0, marginLeft: 12,
|
<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>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* 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 */}
|
{editing ? (
|
||||||
<div style={{
|
<>
|
||||||
display: 'flex', alignItems: 'flex-start', gap: 14,
|
{/* Date */}
|
||||||
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>
|
||||||
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', textTransform: 'capitalize' }}>
|
<div style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>Дата</div>
|
||||||
{formatFullDate(event.start)}
|
<input type="date" value={editDate} onChange={e => setEditDate(e.target.value)} style={{
|
||||||
</div>
|
width: '100%', padding: '12px 16px', borderRadius: 12,
|
||||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4 }}>
|
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)',
|
||||||
{event.allDay
|
color: 'var(--text-primary)', fontSize: 15, outline: 'none', fontFamily: 'inherit',
|
||||||
? 'Весь день'
|
}} />
|
||||||
: `${formatTime(event.start)} — ${formatTime(event.end)}`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Location */}
|
{/* All day toggle */}
|
||||||
{event.location && (
|
<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={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 14,
|
width: 40, height: 22, borderRadius: 11,
|
||||||
padding: '16px 18px', borderRadius: 16,
|
background: editAllDay ? event.color : 'rgba(255,255,255,0.1)',
|
||||||
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
|
position: 'relative', transition: 'background 0.2s ease',
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 42, height: 42, borderRadius: 14,
|
width: 18, height: 18, borderRadius: '50%', background: '#fff',
|
||||||
background: 'rgba(251,146,60,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
position: 'absolute', top: 2, left: editAllDay ? 20 : 2,
|
||||||
}}>
|
transition: 'left 0.2s ease', boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
|
||||||
<MapPin size={20} color="#fb923c" />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 500 }}>{event.location}</div>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description */}
|
{/* Time */}
|
||||||
{event.description && (
|
{!editAllDay && (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}>Время</div>
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '16px 18px', borderRadius: 16,
|
display: 'flex', alignItems: 'center',
|
||||||
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
|
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 }}>
|
<input type="time" value={editStartTime} onChange={e => setEditStartTime(e.target.value)} style={{
|
||||||
<AlignLeft size={15} color="var(--text-secondary)" />
|
flex: 1, padding: '12px 16px', border: 'none', background: 'transparent',
|
||||||
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Описание</span>
|
color: 'var(--text-primary)', fontSize: 16, fontWeight: 600,
|
||||||
</div>
|
fontFamily: 'inherit', outline: 'none', textAlign: 'center',
|
||||||
|
}} />
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 14, color: 'var(--text-primary)', lineHeight: 1.7,
|
padding: '0 10px', color: 'var(--text-tertiary)', fontSize: 12,
|
||||||
whiteSpace: 'pre-wrap', fontWeight: 400,
|
borderLeft: '1px solid rgba(255,255,255,0.06)',
|
||||||
}}>
|
borderRight: '1px solid rgba(255,255,255,0.06)',
|
||||||
{event.description}
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete */}
|
{saveError && (
|
||||||
<div style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: 16, marginTop: 4 }}>
|
<div style={{ color: '#f87171', fontSize: 13, padding: '8px 12px', borderRadius: 10, background: 'rgba(239,68,68,0.08)' }}>{saveError}</div>
|
||||||
{!confirmDelete ? (
|
)}
|
||||||
<button onClick={() => setConfirmDelete(true)} style={{
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
{/* Save / Cancel */}
|
||||||
padding: '13px 18px', borderRadius: 14, width: '100%',
|
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
|
||||||
background: 'rgba(239,68,68,0.06)', border: '1px solid rgba(239,68,68,0.15)',
|
<button onClick={handleSave} disabled={saving} style={{
|
||||||
color: '#f87171', fontSize: 14, fontWeight: 600,
|
flex: 1, padding: '13px', borderRadius: 14,
|
||||||
transition: 'all 0.25s ease',
|
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>
|
</button>
|
||||||
) : (
|
<button onClick={() => { setEditing(false); setEditTitle(event.title) }} style={{
|
||||||
<div style={{ display: 'flex', gap: 10 }}>
|
padding: '13px 20px', borderRadius: 14,
|
||||||
<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)',
|
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)',
|
||||||
color: 'var(--text-secondary)', fontSize: 14, fontWeight: 600,
|
color: 'var(--text-secondary)', fontSize: 14, fontWeight: 600,
|
||||||
}}>
|
}}>
|
||||||
Отмена
|
Отмена
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ————— Day Events Popover —————
|
// ————— Day Events Popover —————
|
||||||
function DayEventsModal({ day, month, year, events, onClose, onSelect }: {
|
function DayEventsModal({ day, month, year, events, onClose, onSelect }: {
|
||||||
day: number; month: number; year: number
|
day: number; month: number; year: number
|
||||||
@@ -734,7 +852,7 @@ export default function CalendarTab() {
|
|||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
<EventDetailModal event={selectedEvent} onClose={() => setSelectedEvent(null)} onDelete={deleteEvent} />
|
<EventDetailModal event={selectedEvent} onClose={() => setSelectedEvent(null)} onDelete={deleteEvent} onUpdate={updateEvent} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{dayPopover && (
|
{dayPopover && (
|
||||||
|
|||||||
@@ -111,7 +111,9 @@ export default function DeviceCard({
|
|||||||
|
|
||||||
{/* Top: icon + toggle */}
|
{/* Top: icon + toggle */}
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', position: 'relative', zIndex: 1 }}>
|
<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,
|
width: 48, height: 48, borderRadius: 16,
|
||||||
background: isOn ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.05)',
|
background: isOn ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.05)',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
|
|||||||
fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase',
|
fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase',
|
||||||
letterSpacing: '0.1em', fontWeight: 600, marginBottom: 14,
|
letterSpacing: '0.1em', fontWeight: 600, marginBottom: 14,
|
||||||
}}>
|
}}>
|
||||||
Прогноз на ближайшие дни
|
Прогноз на неделю
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{weather.forecast.map(day => {
|
{weather.forecast.map(day => {
|
||||||
|
|||||||
Reference in New Issue
Block a user