317 lines
13 KiB
TypeScript
317 lines
13 KiB
TypeScript
'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',
|
||
overflowY: 'auto', padding: 20,
|
||
}}
|
||
>
|
||
<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', maxHeight: '90vh', overflow: 'auto',
|
||
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>
|
||
)}
|
||
</>
|
||
)
|
||
}
|