diff --git a/app/globals.css b/app/globals.css index 8d3c21a..a3df93a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -219,3 +219,83 @@ button:focus-visible { .device-active-breathe { animation: device-breathe 3s ease-in-out infinite; } + +/* ————— Weather animations ————— */ +@keyframes spin-slow { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes cloud-float { + 0%, 100% { transform: translateX(0); } + 50% { transform: translateX(4px); } +} + +@keyframes rain-fall { + 0% { transform: translateY(0); opacity: 0.7; } + 80% { opacity: 0.7; } + 100% { transform: translateY(16px); opacity: 0; } +} + +@keyframes snow-fall { + 0% { transform: translateY(0) rotate(0deg); opacity: 0.8; } + 50% { transform: translateY(10px) translateX(3px) rotate(180deg); opacity: 0.6; } + 100% { transform: translateY(20px) rotate(360deg); opacity: 0; } +} + +@keyframes thunder-flash { + 0%, 100% { opacity: 0; } + 5%, 7% { opacity: 1; } + 6% { opacity: 0.3; } + 50%, 52% { opacity: 0.8; } + 51% { opacity: 0.2; } +} + +@keyframes fog-drift { + 0%, 100% { transform: translateX(0); opacity: 0.4; } + 50% { transform: translateX(8px); opacity: 0.6; } +} + +/* ————— Dynamic weather backgrounds ————— */ +.weather-bg-clear { + --orb1-color: rgba(251, 191, 36, 0.08); + --orb2-color: rgba(245, 158, 11, 0.05); +} +.weather-bg-cloudy { + --orb1-color: rgba(148, 163, 184, 0.08); + --orb2-color: rgba(100, 116, 139, 0.06); +} +.weather-bg-rain { + --orb1-color: rgba(59, 130, 246, 0.08); + --orb2-color: rgba(30, 64, 175, 0.06); +} +.weather-bg-snow { + --orb1-color: rgba(186, 230, 253, 0.1); + --orb2-color: rgba(147, 197, 253, 0.07); +} +.weather-bg-thunder { + --orb1-color: rgba(139, 92, 246, 0.1); + --orb2-color: rgba(88, 28, 135, 0.06); +} +.weather-bg-night { + --orb1-color: rgba(67, 56, 202, 0.06); + --orb2-color: rgba(49, 46, 129, 0.05); +} + +.weather-bg-clear .bg-ambient::before, +.weather-bg-cloudy .bg-ambient::before, +.weather-bg-rain .bg-ambient::before, +.weather-bg-snow .bg-ambient::before, +.weather-bg-thunder .bg-ambient::before, +.weather-bg-night .bg-ambient::before { + background: radial-gradient(circle, var(--orb1-color) 0%, transparent 70%); +} + +.weather-bg-clear .bg-ambient::after, +.weather-bg-cloudy .bg-ambient::after, +.weather-bg-rain .bg-ambient::after, +.weather-bg-snow .bg-ambient::after, +.weather-bg-thunder .bg-ambient::after, +.weather-bg-night .bg-ambient::after { + background: radial-gradient(circle, var(--orb2-color) 0%, transparent 70%); +} diff --git a/app/page.tsx b/app/page.tsx index 7c32234..5ab0ecb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,6 +8,7 @@ import TopBar from '@/components/TopBar' import RoomTabs from '@/components/RoomTabs' import DeviceCard from '@/components/DeviceCard' import CalendarTab from '@/components/CalendarTab' +import WeatherAnimation from '@/components/WeatherAnimation' type Tab = 'home' | 'devices' | 'calendar' | 'settings' @@ -91,6 +92,18 @@ function formatEventTime(iso: string): string { return new Date(iso).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) } +function getWeatherBgClass(desc: string | null): string { + if (!desc) return '' + const d = desc.toLowerCase() + const hour = new Date().getHours() + if (hour >= 22 || hour < 5) return 'weather-bg-night' + if (d.includes('гроз')) return 'weather-bg-thunder' + if (d.includes('снег')) return 'weather-bg-snow' + if (d.includes('дождь') || d.includes('ливен') || d.includes('морос')) return 'weather-bg-rain' + if (d.includes('пасмурн') || d.includes('облач') || d.includes('туман')) return 'weather-bg-cloudy' + return 'weather-bg-clear' +} + function getGreeting(): string { const h = new Date().getHours() if (h >= 5 && h < 12) return 'Доброе утро' @@ -161,7 +174,7 @@ function Screensaver({ weather, onDismiss }: { weather: WeatherData | null; onDi display: 'flex', alignItems: 'center', gap: 12, marginTop: 16, color: 'rgba(255,255,255,0.3)', }}> - {getWeatherIcon(weather.desc)} + {weather.temp}° {weather.desc} @@ -313,7 +326,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
{getWeatherIcon(weather.desc)}
Погода
- {getWeatherIcon(weather.desc)} +
{weather.temp}°
{weather.desc}
@@ -678,7 +691,7 @@ function HomePageInner() { } return ( -
+
diff --git a/components/TopBar.tsx b/components/TopBar.tsx index 0603307..d4559c1 100644 --- a/components/TopBar.tsx +++ b/components/TopBar.tsx @@ -1,7 +1,8 @@ 'use client' import { useState, useEffect } from 'react' -import { Droplets, Wind, Thermometer, X, CloudRain, Snowflake, CloudLightning, Cloud, Sun, CloudSun } from 'lucide-react' +import { Droplets, Wind, Thermometer, X } from 'lucide-react' +import WeatherAnimation from '@/components/WeatherAnimation' interface WeatherData { temp: string @@ -179,13 +180,13 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) { position: 'relative', overflow: 'hidden', }}> {/* Background emoji */} -
- {getWeatherIcon(weather.desc)} +
+
- {getWeatherIcon(weather.desc)} +
{weather.temp}° @@ -275,7 +276,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) { : '1px solid rgba(255,255,255,0.04)', }}> {/* Icon */} - {getWeatherIcon(day.desc)} + {/* Day info */}
diff --git a/components/WeatherAnimation.tsx b/components/WeatherAnimation.tsx new file mode 100644 index 0000000..60cc55b --- /dev/null +++ b/components/WeatherAnimation.tsx @@ -0,0 +1,151 @@ +'use client' + +interface WeatherAnimationProps { + condition: string + size?: number +} + +function getCondition(desc: string): 'clear' | 'partly' | 'cloudy' | 'rain' | 'snow' | 'thunder' | 'fog' { + const d = desc?.toLowerCase() || '' + if (d.includes('гроз')) return 'thunder' + if (d.includes('снег') || d.includes('снегопад')) return 'snow' + if (d.includes('дождь') || d.includes('ливен') || d.includes('морос')) return 'rain' + if (d.includes('туман')) return 'fog' + if (d.includes('пасмурн')) return 'cloudy' + if (d.includes('облач') || d.includes('перем')) return 'partly' + return 'clear' +} + +export default function WeatherAnimation({ condition, size = 64 }: WeatherAnimationProps) { + const c = getCondition(condition) + const s = size + + return ( +
+ + {/* Sun */} + {(c === 'clear' || c === 'partly') && ( + + + {/* Rays */} + {[0,45,90,135,180,225,270,315].map(angle => ( + + ))} + + )} + + {/* Cloud */} + {(c === 'partly' || c === 'cloudy' || c === 'rain' || c === 'snow' || c === 'thunder') && ( + + + + + + + )} + + {/* Rain drops */} + {(c === 'rain' || c === 'thunder') && ( + + {[ + { x: 42, delay: 0 }, + { x: 52, delay: 0.3 }, + { x: 62, delay: 0.6 }, + { x: 47, delay: 0.9 }, + { x: 57, delay: 0.15 }, + ].map((drop, i) => ( + + ))} + + )} + + {/* Thunder bolt */} + {c === 'thunder' && ( + + )} + + {/* Snow flakes */} + {c === 'snow' && ( + + {[ + { x: 42, delay: 0 }, + { x: 52, delay: 0.4 }, + { x: 62, delay: 0.8 }, + { x: 47, delay: 1.2 }, + { x: 57, delay: 0.2 }, + ].map((flake, i) => ( + + ))} + + )} + + {/* Fog lines */} + {c === 'fog' && ( + + {[40, 52, 64].map((y, i) => ( + + ))} + + )} + +
+ ) +}