230 lines
8.4 KiB
TypeScript
230 lines
8.4 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { Cloud, Droplets, Wind } 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
|
||
}
|
||
|
||
function getWeatherEmoji(desc: string): string {
|
||
const d = desc?.toLowerCase() || ''
|
||
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 '⛈️'
|
||
return '🌤️'
|
||
}
|
||
|
||
function formatTime(date: Date): string {
|
||
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
|
||
}
|
||
|
||
function formatDate(date: Date): string {
|
||
return date.toLocaleDateString('ru-RU', { weekday: 'short', day: 'numeric', month: 'short' })
|
||
}
|
||
|
||
export default function TopBar({ weather, sensors }: TopBarProps) {
|
||
const [time, setTime] = useState(() => new Date())
|
||
const [showModal, setShowModal] = useState(false)
|
||
|
||
useEffect(() => {
|
||
const t = setInterval(() => setTime(new Date()), 1000)
|
||
return () => clearInterval(t)
|
||
}, [])
|
||
|
||
return (
|
||
<>
|
||
<header
|
||
style={{
|
||
height: 64,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: '0 20px',
|
||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||
background: 'rgba(255,255,255,0.02)',
|
||
backdropFilter: 'blur(10px)',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{/* Left: time + date */}
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
|
||
<span style={{ fontSize: 22, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.5px' }}>
|
||
{formatTime(time)}
|
||
</span>
|
||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||
{formatDate(time)}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Right: sensors + weather */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||
{/* Room sensors */}
|
||
{sensors && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>🌡️</span>
|
||
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-primary)' }}>
|
||
{sensors.temperature}°
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<Droplets size={13} color="rgba(255,255,255,0.4)" />
|
||
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--text-primary)' }}>
|
||
{sensors.humidity}%
|
||
</span>
|
||
</div>
|
||
{sensors.pm25 !== undefined && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||
<Wind size={13} color="rgba(255,255,255,0.4)" />
|
||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||
PM2.5: {sensors.pm25}
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Divider */}
|
||
{sensors && weather && (
|
||
<div style={{ width: 1, height: 24, background: 'rgba(255,255,255,0.08)' }} />
|
||
)}
|
||
|
||
{/* Weather widget */}
|
||
{weather && (
|
||
<button
|
||
onClick={() => setShowModal(true)}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
padding: '6px 12px',
|
||
borderRadius: 10,
|
||
background: 'rgba(255,255,255,0.05)',
|
||
border: '1px solid rgba(255,255,255,0.08)',
|
||
color: 'var(--text-primary)',
|
||
touchAction: 'manipulation',
|
||
WebkitTapHighlightColor: 'transparent',
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 16 }}>{getWeatherEmoji(weather.desc)}</span>
|
||
<span style={{ fontSize: 16, fontWeight: 600 }}>{weather.temp}°</span>
|
||
<span style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{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.7)',
|
||
backdropFilter: 'blur(8px)',
|
||
zIndex: 100,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<div
|
||
onClick={e => e.stopPropagation()}
|
||
style={{
|
||
background: '#13131f',
|
||
border: '1px solid rgba(255,255,255,0.1)',
|
||
borderRadius: 20,
|
||
padding: 28,
|
||
minWidth: 320,
|
||
maxWidth: 400,
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||
<span style={{ fontSize: 40 }}>{getWeatherEmoji(weather.desc)}</span>
|
||
<div>
|
||
<div style={{ fontSize: 36, fontWeight: 700, lineHeight: 1 }}>{weather.temp}°C</div>
|
||
<div style={{ fontSize: 15, color: 'var(--text-secondary)', marginTop: 2 }}>{weather.desc}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 16, marginBottom: 20 }}>
|
||
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||
<Droplets size={13} style={{ marginRight: 4 }} />
|
||
Влажность: {weather.humidity}%
|
||
</div>
|
||
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||
<Wind size={13} style={{ marginRight: 4 }} />
|
||
Ветер: {weather.windSpeed} км/ч
|
||
</div>
|
||
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>
|
||
Ощущается: {weather.feelsLike}°
|
||
</div>
|
||
</div>
|
||
|
||
{weather.forecast && weather.forecast.length > 0 && (
|
||
<div>
|
||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 12 }}>
|
||
Прогноз
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{weather.forecast.map(day => {
|
||
const d = new Date(day.date)
|
||
const label = d.toLocaleDateString('ru-RU', { weekday: 'short', day: 'numeric', month: 'short' })
|
||
return (
|
||
<div key={day.date} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 80 }}>{label}</span>
|
||
<span style={{ fontSize: 16 }}>{getWeatherEmoji(day.desc)}</span>
|
||
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{day.desc}</span>
|
||
<span style={{ fontSize: 13, color: 'var(--text-primary)', fontWeight: 500 }}>
|
||
{day.maxTemp}° / {day.minTemp}°
|
||
</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => setShowModal(false)}
|
||
style={{
|
||
marginTop: 20,
|
||
width: '100%',
|
||
padding: '10px',
|
||
borderRadius: 10,
|
||
background: 'rgba(255,255,255,0.05)',
|
||
color: 'var(--text-secondary)',
|
||
fontSize: 14,
|
||
touchAction: 'manipulation',
|
||
}}
|
||
>
|
||
Закрыть
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
}
|