feat: premium UI redesign — glassmorphism, gradient accents, ambient background
All checks were successful
Deploy / deploy (push) Successful in 2m40s
All checks were successful
Deploy / deploy (push) Successful in 2m40s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Cloud, Droplets, Wind } from 'lucide-react'
|
||||
import { Droplets, Wind, Thermometer, X } from 'lucide-react'
|
||||
|
||||
interface WeatherData {
|
||||
temp: string
|
||||
@@ -23,7 +23,7 @@ interface TopBarProps {
|
||||
sensors: SensorData | null
|
||||
}
|
||||
|
||||
function getWeatherEmoji(desc: string): string {
|
||||
function getWeatherIcon(desc: string): string {
|
||||
const d = desc?.toLowerCase() || ''
|
||||
if (d.includes('ясно') || d.includes('солнеч')) return '☀️'
|
||||
if (d.includes('облач')) return '⛅'
|
||||
@@ -39,7 +39,10 @@ function formatTime(date: Date): string {
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString('ru-RU', { weekday: 'short', day: 'numeric', month: 'short' })
|
||||
const weekday = date.toLocaleDateString('ru-RU', { weekday: 'long' })
|
||||
const day = date.getDate()
|
||||
const month = date.toLocaleDateString('ru-RU', { month: 'long' })
|
||||
return `${weekday}, ${day} ${month}`
|
||||
}
|
||||
|
||||
export default function TopBar({ weather, sensors }: TopBarProps) {
|
||||
@@ -55,60 +58,67 @@ export default function TopBar({ weather, sensors }: TopBarProps) {
|
||||
<>
|
||||
<header
|
||||
style={{
|
||||
height: 64,
|
||||
height: 72,
|
||||
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)',
|
||||
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: 10 }}>
|
||||
<span style={{ fontSize: 22, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.5px' }}>
|
||||
<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: 13, color: 'var(--text-secondary)' }}>
|
||||
<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: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{/* 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 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>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
{sensors && weather && (
|
||||
<div style={{ width: 1, height: 24, background: 'rgba(255,255,255,0.08)' }} />
|
||||
)}
|
||||
|
||||
{/* Weather widget */}
|
||||
{weather && (
|
||||
<button
|
||||
@@ -116,19 +126,18 @@ export default function TopBar({ weather, sensors }: TopBarProps) {
|
||||
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)',
|
||||
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)',
|
||||
touchAction: 'manipulation',
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
transition: 'all 0.25s ease',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
@@ -142,7 +151,7 @@ export default function TopBar({ weather, sensors }: TopBarProps) {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -152,75 +161,84 @@ export default function TopBar({ weather, sensors }: TopBarProps) {
|
||||
<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,
|
||||
background: 'rgba(18, 18, 35, 0.95)',
|
||||
backdropFilter: 'blur(40px)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: 24,
|
||||
padding: 32,
|
||||
minWidth: 360,
|
||||
maxWidth: 420,
|
||||
boxShadow: '0 25px 60px rgba(0,0,0,0.5)',
|
||||
}}
|
||||
>
|
||||
<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 style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<span style={{ fontSize: 48 }}>{getWeatherIcon(weather.desc)}</span>
|
||||
<div>
|
||||
<div style={{ fontSize: 40, fontWeight: 800, lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
|
||||
<div style={{ fontSize: 15, color: 'var(--text-secondary)', marginTop: 4, fontWeight: 500 }}>{weather.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setShowModal(false)} style={{ color: 'var(--text-secondary)', padding: 4 }}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</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 style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: 10,
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
{[
|
||||
{ icon: <Droplets size={16} />, label: 'Влажность', value: `${weather.humidity}%` },
|
||||
{ icon: <Wind size={16} />, label: 'Ветер', value: `${weather.windSpeed} км/ч` },
|
||||
{ icon: <Thermometer size={16} />, label: 'Ощущается', value: `${weather.feelsLike}°` },
|
||||
].map(item => (
|
||||
<div key={item.label} style={{
|
||||
padding: '14px 12px',
|
||||
borderRadius: 16,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ color: 'var(--text-secondary)', marginBottom: 8, display: 'flex', justifyContent: 'center' }}>{item.icon}</div>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--text-primary)' }}>{item.value}</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', marginTop: 4 }}>{item.label}</div>
|
||||
</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 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 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{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}°
|
||||
<div key={day.date} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 14px',
|
||||
borderRadius: 12,
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
}}>
|
||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 100, textTransform: 'capitalize' }}>{label}</span>
|
||||
<span style={{ fontSize: 18 }}>{getWeatherIcon(day.desc)}</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--text-secondary)', minWidth: 80, textAlign: 'center' }}>{day.desc}</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||
{day.maxTemp}° <span style={{ color: 'var(--text-secondary)', fontWeight: 400 }}>/ {day.minTemp}°</span>
|
||||
</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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user