feat: remove weather from TopBar, clickable forecast days with detail modal (feels like, humidity, wind, precip)
All checks were successful
Deploy / deploy (push) Successful in 3m8s
All checks were successful
Deploy / deploy (push) Successful in 3m8s
This commit is contained in:
95
app/page.tsx
95
app/page.tsx
@@ -19,7 +19,7 @@ interface WeatherData {
|
||||
humidity: string
|
||||
windSpeed: string
|
||||
feelsLike: string
|
||||
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string }[]
|
||||
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string; feelsLikeMax?: string; feelsLikeMin?: string; precipProb?: string; windSpeed?: string; humidity?: string }[]
|
||||
}
|
||||
|
||||
interface SensorData {
|
||||
@@ -285,12 +285,91 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) {
|
||||
}
|
||||
|
||||
// ————— Home Tab —————
|
||||
// ————— Weather Day Detail Modal —————
|
||||
function WeatherDayModal({ day, current, onClose }: {
|
||||
day: { date: string; maxTemp: string; minTemp: string; desc: string; feelsLikeMax?: string; feelsLikeMin?: string; precipProb?: string; windSpeed?: string; humidity?: string }
|
||||
current: WeatherData | null
|
||||
onClose: () => void
|
||||
}) {
|
||||
const d = new Date(day.date)
|
||||
const isToday = d.toDateString() === new Date().toDateString()
|
||||
const dayLabel = isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' })
|
||||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(12px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={onClose}>
|
||||
<div style={{
|
||||
background: 'rgba(16,16,30,0.97)', backdropFilter: 'blur(40px)',
|
||||
border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28,
|
||||
width: 400, maxWidth: '90vw', overflow: 'hidden',
|
||||
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
|
||||
}} onClick={e => e.stopPropagation()}>
|
||||
|
||||
{/* Hero */}
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(59,130,246,0.12), rgba(99,102,241,0.06))',
|
||||
borderBottom: '1px solid rgba(59,130,246,0.1)',
|
||||
padding: '28px 28px 24px', textAlign: 'center',
|
||||
position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: -10, right: 10, opacity: 0.1, pointerEvents: 'none' }}>
|
||||
<WeatherAnimation condition={day.desc} size={100} />
|
||||
</div>
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', fontWeight: 500, textTransform: 'capitalize', marginBottom: 12 }}>{dayLabel}</div>
|
||||
<WeatherAnimation condition={day.desc} size={64} />
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 8, marginTop: 12 }}>
|
||||
<span style={{ fontSize: 42, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-2px' }}>{day.maxTemp}°</span>
|
||||
<span style={{ fontSize: 22, fontWeight: 500, color: 'var(--text-secondary)' }}>{day.minTemp}°</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 15, color: 'var(--text-secondary)', marginTop: 6, fontWeight: 500 }}>{day.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details grid */}
|
||||
<div style={{ padding: '22px 28px 28px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10 }}>
|
||||
{[
|
||||
{ icon: <Thermometer size={18} color="#fb923c" />, bg: 'rgba(251,146,60,0.1)', label: 'Ощущается', value: `${day.feelsLikeMax || '—'}° / ${day.feelsLikeMin || '—'}°` },
|
||||
{ icon: <Droplets size={18} color="#3b82f6" />, bg: 'rgba(59,130,246,0.1)', label: 'Влажность', value: `${day.humidity || (isToday && current ? current.humidity : '—')}%` },
|
||||
{ icon: <Wind size={18} color="#22d3ee" />, bg: 'rgba(34,211,238,0.1)', label: 'Ветер', value: `${day.windSpeed || (isToday && current ? current.windSpeed : '—')} м/с` },
|
||||
{ icon: <span style={{ fontSize: 18 }}>🌧️</span>, bg: 'rgba(99,102,241,0.1)', label: 'Вероятность осадков', value: `${day.precipProb || '0'}%` },
|
||||
].map(item => (
|
||||
<div key={item.label} style={{
|
||||
padding: '16px 14px', borderRadius: 16,
|
||||
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 38, height: 38, borderRadius: 12, background: item.bg,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>{item.icon}</div>
|
||||
<div style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)' }}>{item.value}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textAlign: 'center' }}>{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button onClick={onClose} style={{
|
||||
width: '100%', padding: '13px', borderRadius: 14, marginTop: 16,
|
||||
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)',
|
||||
color: 'var(--text-secondary)', fontSize: 14, fontWeight: 600,
|
||||
}}>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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[]>([])
|
||||
const [selectedDay, setSelectedDay] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/calendar?range=today')
|
||||
@@ -376,7 +455,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
</div>
|
||||
|
||||
{/* Current */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1 }}>
|
||||
<div onClick={() => weather?.forecast?.[0] && setSelectedDay(weather.forecast[0])} style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1, cursor: 'pointer' }}>
|
||||
<WeatherAnimation condition={weather.desc} size={48} />
|
||||
<div>
|
||||
<div style={{ fontSize: 32, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
|
||||
@@ -394,10 +473,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
const d = new Date(day.date)
|
||||
const isToday = idx === 0
|
||||
return (
|
||||
<div key={day.date} style={{
|
||||
<div key={day.date} onClick={() => setSelectedDay(day)} style={{
|
||||
flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px',
|
||||
borderRadius: 10,
|
||||
borderRadius: 10, cursor: 'pointer',
|
||||
background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent',
|
||||
transition: 'background 0.2s ease',
|
||||
}}>
|
||||
<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)}
|
||||
@@ -523,6 +603,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Weather day detail modal */}
|
||||
{selectedDay && (
|
||||
<WeatherDayModal day={selectedDay} current={weather} onClose={() => setSelectedDay(null)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -848,7 +933,7 @@ function HomePageInner() {
|
||||
<Sidebar active={tab} onChange={setTab} />
|
||||
|
||||
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0, position: 'relative', zIndex: 1 }}>
|
||||
<TopBar weather={weather} sensors={sensors} haConnected={haConnected} />
|
||||
<TopBar sensors={sensors} haConnected={haConnected} />
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{tab === 'home' && (
|
||||
|
||||
Reference in New Issue
Block a user