Files
smart-home-tablet/components/TopBar.tsx
Cosmo 690db4c6cf
Some checks failed
Deploy / deploy (push) Has been cancelled
feat: event editing, light/dark theme, device animations, 7-day forecast
2026-04-22 19:56:38 +00:00

315 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { Droplets, Wind, Thermometer, X, CloudRain, Snowflake, CloudLightning, Cloud, Sun, CloudSun } from 'lucide-react'
interface WeatherData {
temp: string
desc: string
humidity: string
windSpeed: string
feelsLike: string
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string }[]
}
interface SensorData {
temperature: number
humidity: number
pm25: number
}
interface TopBarProps {
weather: WeatherData | null
sensors: SensorData | null
haConnected?: boolean
}
function getWeatherIcon(desc: string): string {
const d = desc?.toLowerCase() || ''
if (d.includes('ясно') || d.includes('солнеч')) return '☀️'
if (d.includes('облач') || d.includes('перем')) return '⛅'
if (d.includes('пасмурн')) return '☁️'
if (d.includes('морос')) return '🌦️'
if (d.includes('дождь') || d.includes('ливен')) return '🌧️'
if (d.includes('снег')) return '🌨️'
if (d.includes('гроз')) return '⛈️'
if (d.includes('туман')) return '🌫️'
return '🌤️'
}
function formatTime(date: Date): string {
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
}
function formatDate(date: Date): string {
const weekday = date.toLocaleDateString('ru-RU', { weekday: 'long' })
const day = date.getDate()
const month = date.toLocaleDateString('ru-RU', { month: 'long' })
return `${weekday}, ${day} ${month}`
}
function getWindDesc(ms: number): string {
if (ms <= 1) return 'Штиль'
if (ms <= 3) return 'Тихий'
if (ms <= 5) return 'Лёгкий'
if (ms <= 8) return 'Умеренный'
if (ms <= 11) return 'Свежий'
if (ms <= 14) return 'Сильный'
return 'Шторм'
}
export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
const [time, setTime] = useState(() => new Date())
const [showModal, setShowModal] = useState(false)
useEffect(() => {
const t = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(t)
}, [])
const windMs = weather ? parseInt(weather.windSpeed) || 0 : 0
return (
<>
<header
style={{
height: 72,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
borderBottom: '1px solid rgba(255,255,255,0.04)',
background: 'transparent',
flexShrink: 0,
position: 'relative',
zIndex: 5,
}}
>
{/* Left: time + date */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12 }}>
<span style={{
fontSize: 28, fontWeight: 700, color: 'var(--text-primary)',
letterSpacing: '-1px', fontVariantNumeric: 'tabular-nums',
}}>
{formatTime(time)}
</span>
<span style={{
fontSize: 14, color: 'var(--text-secondary)',
fontWeight: 400, textTransform: 'capitalize',
}}>
{formatDate(time)}
</span>
</div>
{/* Right: sensors + weather */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{/* HA status */}
<div title={haConnected ? 'Home Assistant подключён' : 'Home Assistant недоступен'} style={{
width: 10, height: 10, borderRadius: '50%',
background: haConnected ? '#34d399' : '#f87171',
boxShadow: haConnected ? '0 0 8px rgba(52,211,153,0.5)' : '0 0 8px rgba(248,113,113,0.5)',
transition: 'all 0.5s ease',
flexShrink: 0,
}} />
{sensors && (
<div style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '8px 14px', borderRadius: 14,
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
}}>
<Thermometer size={14} color="rgba(255,255,255,0.35)" />
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', marginRight: 8 }}>
{sensors.temperature}°
</span>
<Droplets size={14} color="rgba(255,255,255,0.35)" />
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)', marginRight: 8 }}>
{sensors.humidity}%
</span>
<Wind size={14} color="rgba(255,255,255,0.35)" />
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
{sensors.pm25}
</span>
</div>
)}
{weather && (
<button
onClick={() => setShowModal(true)}
style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 16px', borderRadius: 14,
background: 'linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.08))',
border: '1px solid rgba(129,140,248,0.15)',
color: 'var(--text-primary)', transition: 'all 0.25s ease',
}}
>
<span style={{ fontSize: 20 }}>{getWeatherIcon(weather.desc)}</span>
<span style={{ fontSize: 15, fontWeight: 700 }}>{weather.temp}°</span>
<span style={{ fontSize: 12, color: 'var(--text-secondary)', fontWeight: 500 }}>{weather.desc}</span>
</button>
)}
</div>
</header>
{/* Weather Modal */}
{showModal && weather && (
<div
onClick={() => setShowModal(false)}
style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(12px)',
zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<div
onClick={e => e.stopPropagation()}
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',
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
}}
>
{/* Hero section */}
<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: '32px 36px 28px',
position: 'relative', overflow: 'hidden',
}}>
{/* Background emoji */}
<div style={{ position: 'absolute', top: -10, right: 10, fontSize: 100, opacity: 0.08, pointerEvents: 'none' }}>
{getWeatherIcon(weather.desc)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
<span style={{ fontSize: 56 }}>{getWeatherIcon(weather.desc)}</span>
<div>
<div style={{ fontSize: 48, fontWeight: 800, lineHeight: 1, letterSpacing: '-3px', color: 'var(--text-primary)' }}>
{weather.temp}°
</div>
<div style={{ fontSize: 16, color: 'var(--text-secondary)', marginTop: 6, fontWeight: 500 }}>
{weather.desc}
</div>
</div>
</div>
<button onClick={() => setShowModal(false)} style={{
color: 'var(--text-secondary)', padding: 8, borderRadius: 12,
background: 'rgba(255,255,255,0.05)',
}}>
<X size={18} />
</button>
</div>
</div>
{/* Details */}
<div style={{ padding: '24px 36px 32px' }}>
{/* Stats grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10, marginBottom: 28 }}>
{[
{
icon: <Thermometer size={18} color="#fb923c" />,
bg: 'rgba(251,146,60,0.1)',
label: 'Ощущается',
value: `${weather.feelsLike}°`,
},
{
icon: <Droplets size={18} color="#3b82f6" />,
bg: 'rgba(59,130,246,0.1)',
label: 'Влажность',
value: `${weather.humidity}%`,
},
{
icon: <Wind size={18} color="#22d3ee" />,
bg: 'rgba(34,211,238,0.1)',
label: getWindDesc(windMs),
value: `${weather.windSpeed} м/с`,
},
].map(item => (
<div key={item.label} style={{
padding: '18px 14px', borderRadius: 18,
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.05)',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10,
}}>
<div style={{
width: 40, height: 40, borderRadius: 12,
background: item.bg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{item.icon}
</div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)' }}>{item.value}</div>
<div style={{ fontSize: 11, color: 'var(--text-secondary)', fontWeight: 500 }}>{item.label}</div>
</div>
))}
</div>
{/* Forecast */}
{weather.forecast && weather.forecast.length > 0 && (
<>
<div style={{
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 => {
const d = new Date(day.date)
const isToday = d.toDateString() === new Date().toDateString()
const weekday = d.toLocaleDateString('ru-RU', { weekday: 'long' })
const dateStr = d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' })
return (
<div key={day.date} style={{
display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 18px', borderRadius: 16,
background: isToday
? 'linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.05))'
: 'rgba(255,255,255,0.025)',
border: isToday
? '1px solid rgba(129,140,248,0.15)'
: '1px solid rgba(255,255,255,0.04)',
}}>
{/* Icon */}
<span style={{ fontSize: 28, flexShrink: 0 }}>{getWeatherIcon(day.desc)}</span>
{/* Day info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: 14, fontWeight: 600, color: 'var(--text-primary)',
textTransform: 'capitalize',
}}>
{isToday ? 'Сегодня' : weekday}
</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>
{dateStr} · {day.desc}
</div>
</div>
{/* Temps */}
<div style={{ textAlign: 'right', flexShrink: 0 }}>
<span style={{ fontSize: 18, fontWeight: 700, color: 'var(--text-primary)' }}>
{day.maxTemp}°
</span>
<span style={{ fontSize: 14, color: 'var(--text-secondary)', fontWeight: 400, marginLeft: 4 }}>
{day.minTemp}°
</span>
</div>
</div>
)
})}
</div>
</>
)}
</div>
</div>
</div>
)}
</>
)
}