feat: redesigned Home (weather+forecast bar, today+tomorrow, pinned notes), fix snow animation, scrollable weather modal, weather hints
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -11,6 +11,7 @@ interface Note {
|
||||
items?: { id: string; text: string; done: boolean }[]
|
||||
text?: string
|
||||
color: string
|
||||
pinDate: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
@@ -42,6 +43,7 @@ export async function POST(req: Request) {
|
||||
items: body.type === 'shopping' ? [] : undefined,
|
||||
text: body.type === 'note' ? '' : undefined,
|
||||
color: body.color || '#6366f1',
|
||||
pinDate: body.pinDate || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
@@ -238,9 +238,11 @@ button:focus-visible {
|
||||
}
|
||||
|
||||
@keyframes snow-fall {
|
||||
0% { transform: translateY(0) rotate(0deg); opacity: 0.8; }
|
||||
50% { transform: translateY(10px) translateX(3px) rotate(180deg); opacity: 0.6; }
|
||||
100% { transform: translateY(20px) rotate(360deg); opacity: 0; }
|
||||
0% { transform: translateY(0) translateX(0); opacity: 0.9; }
|
||||
25% { transform: translateY(5px) translateX(2px); opacity: 0.8; }
|
||||
50% { transform: translateY(10px) translateX(-1px); opacity: 0.7; }
|
||||
75% { transform: translateY(15px) translateX(3px); opacity: 0.4; }
|
||||
100% { transform: translateY(22px) translateX(0); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes thunder-flash {
|
||||
|
||||
238
app/page.tsx
238
app/page.tsx
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete, KeyRound, MapPin, Info, Check, X as XIcon } from 'lucide-react'
|
||||
import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete, KeyRound, MapPin, Info, Check, X as XIcon, StickyNote, ShoppingCart, FileText } from 'lucide-react'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import TopBar from '@/components/TopBar'
|
||||
import RoomTabs from '@/components/RoomTabs'
|
||||
@@ -287,8 +287,10 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) {
|
||||
// ————— Home Tab —————
|
||||
function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) {
|
||||
const [todayEvents, setTodayEvents] = useState<CalendarEvent[]>([])
|
||||
const [tomorrowEvents, setTomorrowEvents] = useState<CalendarEvent[]>([])
|
||||
const [calLoading, setCalLoading] = useState(true)
|
||||
const [greeting, setGreeting] = useState(getGreeting())
|
||||
const [pinnedNotes, setPinnedNotes] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/calendar?range=today')
|
||||
@@ -296,6 +298,28 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
.then(d => setTodayEvents(d.events || []))
|
||||
.catch(() => setTodayEvents([]))
|
||||
.finally(() => setCalLoading(false))
|
||||
|
||||
// Tomorrow
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
const y = tomorrow.getFullYear()
|
||||
const m = tomorrow.getMonth()
|
||||
fetch(`/api/calendar?range=month&year=${y}&month=${m}`)
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
const tmr = (d.events || []).filter((e: any) => {
|
||||
const ed = new Date(e.start)
|
||||
return ed.getDate() === tomorrow.getDate() && ed.getMonth() === tomorrow.getMonth()
|
||||
})
|
||||
setTomorrowEvents(tmr)
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
// Notes
|
||||
fetch('/api/notes')
|
||||
.then(r => r.json())
|
||||
.then(d => setPinnedNotes((d.notes || []).slice(0, 3)))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -303,54 +327,84 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
const pm25Info = sensors ? getPm25Level(sensors.pm25) : null
|
||||
// Weather hint
|
||||
const getWeatherHint = (): string | null => {
|
||||
if (!weather) return null
|
||||
const desc = weather.desc.toLowerCase()
|
||||
const temp = parseInt(weather.temp)
|
||||
if (desc.includes('дождь') || desc.includes('ливен') || desc.includes('морос')) return '☂️ Не забудьте зонт'
|
||||
if (desc.includes('снег')) return '🧤 На улице снег, одевайтесь теплее'
|
||||
if (desc.includes('гроз')) return '⛈️ Ожидается гроза'
|
||||
if (temp <= 0) return '🥶 На улице мороз'
|
||||
if (temp <= 5) return '🧥 Оденьтесь потеплее'
|
||||
if (temp >= 30) return '🥵 Очень жарко, пейте воду'
|
||||
return null
|
||||
}
|
||||
|
||||
const hint = getWeatherHint()
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '20px 24px 28px', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{/* Greeting */}
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.5px', margin: 0 }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '20px 24px 28px', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{/* Greeting + hint */}
|
||||
<div>
|
||||
<h1 style={{ fontSize: 26, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.5px', margin: 0 }}>
|
||||
{greeting} 👋
|
||||
</h1>
|
||||
<p style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4, fontWeight: 400 }}>
|
||||
Вот что происходит дома
|
||||
</p>
|
||||
{hint && (
|
||||
<div style={{
|
||||
fontSize: 14, color: 'var(--text-primary)', marginTop: 8,
|
||||
padding: '10px 16px', borderRadius: 12,
|
||||
background: 'rgba(251,191,36,0.08)', border: '1px solid rgba(251,191,36,0.15)',
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
{hint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
{/* Weather — full width compact */}
|
||||
{weather && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(139,92,246,0.06))',
|
||||
backdropFilter: 'blur(20px)', border: '1px solid rgba(129,140,248,0.12)',
|
||||
borderRadius: 22, padding: '22px 24px', position: 'relative', overflow: 'hidden',
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.05))',
|
||||
backdropFilter: 'blur(20px)', border: '1px solid rgba(129,140,248,0.1)',
|
||||
borderRadius: 20, padding: '18px 22px',
|
||||
display: 'flex', alignItems: 'center', gap: 20,
|
||||
position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: -20, right: -10, fontSize: 80, opacity: 0.12, pointerEvents: 'none' }}>{getWeatherIcon(weather.desc)}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>Погода</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 18, position: 'relative', zIndex: 1 }}>
|
||||
<WeatherAnimation condition={weather.desc} size={56} />
|
||||
<div style={{ position: 'absolute', top: -15, right: 5, opacity: 0.1, pointerEvents: 'none' }}>
|
||||
<WeatherAnimation condition={weather.desc} size={90} />
|
||||
</div>
|
||||
|
||||
{/* Current */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1 }}>
|
||||
<WeatherAnimation condition={weather.desc} size={48} />
|
||||
<div>
|
||||
<div style={{ fontSize: 36, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4, fontWeight: 500 }}>{weather.desc}</div>
|
||||
<div style={{ fontSize: 32, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--text-secondary)', marginTop: 3, fontWeight: 500 }}>{weather.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{weather.forecast && weather.forecast.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', WebkitOverflowScrolling: 'touch' as any, scrollbarWidth: 'none' as any, msOverflowStyle: 'none' as any, paddingBottom: 2 }}>
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ width: 1, height: 50, background: 'rgba(255,255,255,0.08)', flexShrink: 0 }} />
|
||||
|
||||
{/* 7 day forecast */}
|
||||
{weather.forecast && (
|
||||
<div style={{ display: 'flex', gap: 4, flex: 1, overflow: 'hidden' }}>
|
||||
{weather.forecast.map((day, idx) => {
|
||||
const d = new Date(day.date)
|
||||
const isToday = idx === 0
|
||||
return (
|
||||
<div key={day.date} style={{
|
||||
minWidth: 58, background: isToday ? 'rgba(99,102,241,0.1)' : 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 12, padding: '8px 6px', textAlign: 'center',
|
||||
border: isToday ? '1px solid rgba(129,140,248,0.2)' : '1px solid rgba(255,255,255,0.04)',
|
||||
flexShrink: 0,
|
||||
flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px',
|
||||
borderRadius: 10,
|
||||
background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent',
|
||||
}}>
|
||||
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 3, fontWeight: 600 }}>
|
||||
{isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'short' })}
|
||||
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', fontWeight: 600, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).slice(0, 2)}
|
||||
</div>
|
||||
<div style={{ fontSize: 16, marginBottom: 3 }}>{getWeatherIcon(day.desc)}</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-secondary)' }}>{day.minTemp}°</div>
|
||||
<div style={{ fontSize: 14, marginBottom: 2 }}>{getWeatherIcon(day.desc)}</div>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div>
|
||||
<div style={{ fontSize: 9, color: 'var(--text-secondary)' }}>{day.minTemp}°</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -358,49 +412,58 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{sensors && (
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', backdropFilter: 'blur(20px)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>Климат в квартире</div>
|
||||
|
||||
{/* Two columns: Events + Notes */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, flex: 1, minHeight: 0 }}>
|
||||
|
||||
{/* Left: Today + Tomorrow events */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 16, background: 'linear-gradient(135deg, rgba(251,146,60,0.15), rgba(245,158,11,0.08))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Thermometer size={22} color="#fb923c" /></div>
|
||||
<div><div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1 }}>{sensors.temperature}°C</div><div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>Температура</div></div>
|
||||
{/* Today */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 20, padding: '18px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<Calendar size={14} color="var(--text-secondary)" />
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Сегодня</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{todayEvents.length}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 16, background: 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(99,102,241,0.08))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Droplets size={22} color="#3b82f6" /></div>
|
||||
<div><div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1 }}>{sensors.humidity}%</div><div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>Влажность</div></div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 16, background: pm25Info?.bg || 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Wind size={22} color={pm25Info?.color || '#999'} /></div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}><span style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1 }}>{sensors.pm25}</span><span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>µg/m³</span></div>
|
||||
<div style={{ fontSize: 12, color: pm25Info?.color, marginTop: 2, fontWeight: 500 }}>PM2.5 · {pm25Info?.label}</div>
|
||||
{calLoading ? (
|
||||
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>Загрузка...</div>
|
||||
) : todayEvents.length === 0 ? (
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', textAlign: 'center', padding: '8px 0' }}>Свободный день</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{todayEvents.map(ev => (
|
||||
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, background: `${ev.color}08`, border: `1px solid ${ev.color}12` }}>
|
||||
<div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 30, flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 }}>
|
||||
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · <span style={{ color: ev.color }}>{ev.ownerName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', backdropFilter: 'blur(20px)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<Calendar size={15} color="var(--text-secondary)" />
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600 }}>Сегодня</span>
|
||||
{/* Tomorrow */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)', borderRadius: 20, padding: '18px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<Calendar size={14} color="var(--text-secondary)" />
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Завтра</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>{tomorrowEvents.length}</span>
|
||||
</div>
|
||||
{calLoading ? (
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Загрузка...</div>
|
||||
) : todayEvents.length === 0 ? (
|
||||
<div style={{ fontSize: 15, color: 'var(--text-secondary)', textAlign: 'center', padding: '12px 0' }}>Нет событий на сегодня</div>
|
||||
{tomorrowEvents.length === 0 ? (
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', textAlign: 'center', padding: '8px 0' }}>Нет событий</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{todayEvents.map(ev => (
|
||||
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '12px 16px', borderRadius: 14, background: `${ev.color}0a`, border: `1px solid ${ev.color}18` }}>
|
||||
<div style={{ width: 4, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 36, flexShrink: 0 }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{tomorrowEvents.map(ev => (
|
||||
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, background: `${ev.color}06` }}>
|
||||
<div style={{ width: 3, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 24, flexShrink: 0, opacity: 0.6 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 3, display: 'flex', gap: 8 }}>
|
||||
<span>{ev.allDay ? 'Весь день' : `${formatEventTime(ev.start)} — ${formatEventTime(ev.end)}`}</span>
|
||||
<span style={{ color: ev.color, fontWeight: 500 }}>{ev.ownerName}</span>
|
||||
<div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 }}>
|
||||
{ev.allDay ? 'Весь день' : formatEventTime(ev.start)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -409,9 +472,62 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Pinned notes / shopping lists */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{pinnedNotes.length === 0 ? (
|
||||
<div style={{ background: 'rgba(255,255,255,0.02)', border: '1px solid rgba(255,255,255,0.04)', borderRadius: 20, padding: '18px 20px', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center', color: 'var(--text-secondary)' }}>
|
||||
<StickyNote size={24} style={{ margin: '0 auto 8px', opacity: 0.3 }} />
|
||||
<div style={{ fontSize: 13 }}>Заметки появятся здесь</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
pinnedNotes.map(note => {
|
||||
const doneCount = note.items?.filter((i: any) => i.done).length || 0
|
||||
const totalCount = note.items?.length || 0
|
||||
return (
|
||||
<div key={note.id} style={{
|
||||
background: `${note.color}08`, border: `1px solid ${note.color}15`,
|
||||
borderRadius: 20, padding: '18px 20px',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
{note.type === 'shopping' ? <ShoppingCart size={14} color={note.color} /> : <FileText size={14} color={note.color} />}
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{note.title}</span>
|
||||
{note.type === 'shopping' && totalCount > 0 && (
|
||||
<span style={{ fontSize: 11, color: note.color, marginLeft: 'auto' }}>{doneCount}/{totalCount}</span>
|
||||
)}
|
||||
</div>
|
||||
{note.type === 'shopping' ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{(note.items || []).filter((i: any) => !i.done).slice(0, 5).map((item: any) => (
|
||||
<div key={item.id} style={{ fontSize: 13, color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: 2, border: `1.5px solid ${note.color}50`, flexShrink: 0 }} />
|
||||
{item.text}
|
||||
</div>
|
||||
))}
|
||||
{(note.items || []).filter((i: any) => !i.done).length > 5 && (
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
|
||||
+{(note.items || []).filter((i: any) => !i.done).length - 5} ещё
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.5, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 4, WebkitBoxOrient: 'vertical' as any }}>
|
||||
{note.text || 'Пустая заметка'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// ————— Settings Tab —————
|
||||
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)
|
||||
|
||||
@@ -161,6 +161,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
|
||||
position: 'fixed', inset: 0,
|
||||
background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(12px)',
|
||||
zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
overflowY: 'auto', padding: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -168,7 +169,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
|
||||
style={{
|
||||
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',
|
||||
width: 480, maxWidth: '95vw', maxHeight: '90vh', overflow: 'auto',
|
||||
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -106,23 +106,22 @@ export default function WeatherAnimation({ condition, size = 64 }: WeatherAnimat
|
||||
{c === 'snow' && (
|
||||
<g>
|
||||
{[
|
||||
{ x: 42, delay: 0 },
|
||||
{ x: 52, delay: 0.4 },
|
||||
{ x: 62, delay: 0.8 },
|
||||
{ x: 47, delay: 1.2 },
|
||||
{ x: 57, delay: 0.2 },
|
||||
{ x: 44, delay: 0, size: 3 },
|
||||
{ x: 54, delay: 0.5, size: 2.5 },
|
||||
{ x: 64, delay: 1.0, size: 2 },
|
||||
{ x: 49, delay: 1.5, size: 2.5 },
|
||||
{ x: 59, delay: 0.3, size: 3 },
|
||||
].map((flake, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={flake.x} cy={72}
|
||||
r={2.5}
|
||||
<g key={i} style={{ animation: `snow-fall 2.5s linear infinite`, animationDelay: `${flake.delay}s` }}>
|
||||
<text
|
||||
x={flake.x} y={70}
|
||||
fontSize={flake.size * 4}
|
||||
fill="white"
|
||||
opacity={0.8}
|
||||
style={{
|
||||
animation: `snow-fall 2s ease-in-out infinite`,
|
||||
animationDelay: `${flake.delay}s`,
|
||||
}}
|
||||
/>
|
||||
opacity={0.85}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
>❄</text>
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user