Files
smart-home-tablet/components/TopBar.tsx
Cosmo 494126c7d4
Some checks failed
Deploy / deploy (push) Failing after 2m6s
feat: animated SVG weather icons + dynamic gradient background by weather/time
2026-04-22 20:09:13 +00:00

316 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 } from 'lucide-react'
import WeatherAnimation from '@/components/WeatherAnimation'
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: -5, right: 5, opacity: 0.1, pointerEvents: 'none' }}>
<WeatherAnimation condition={weather.desc} size={120} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', position: 'relative', zIndex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
<WeatherAnimation condition={weather.desc} size={72} />
<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 */}
<WeatherAnimation condition={day.desc} size={36} />
{/* 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>
)}
</>
)
}