feat: animated SVG weather icons + dynamic gradient background by weather/time
Some checks failed
Deploy / deploy (push) Failing after 2m6s

This commit is contained in:
Cosmo
2026-04-22 20:09:13 +00:00
parent 408be1d0c4
commit 494126c7d4
4 changed files with 253 additions and 8 deletions

View File

@@ -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 */}
<div style={{ position: 'absolute', top: -10, right: 10, fontSize: 100, opacity: 0.08, pointerEvents: 'none' }}>
{getWeatherIcon(weather.desc)}
<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 }}>
<span style={{ fontSize: 56 }}>{getWeatherIcon(weather.desc)}</span>
<WeatherAnimation condition={weather.desc} size={72} />
<div>
<div style={{ fontSize: 48, fontWeight: 800, lineHeight: 1, letterSpacing: '-3px', color: 'var(--text-primary)' }}>
{weather.temp}°
@@ -275,7 +276,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
: '1px solid rgba(255,255,255,0.04)',
}}>
{/* Icon */}
<span style={{ fontSize: 28, flexShrink: 0 }}>{getWeatherIcon(day.desc)}</span>
<WeatherAnimation condition={day.desc} size={36} />
{/* Day info */}
<div style={{ flex: 1, minWidth: 0 }}>

View File

@@ -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 (
<div style={{ width: s, height: s, position: 'relative', flexShrink: 0 }}>
<svg viewBox="0 0 100 100" width={s} height={s} style={{ overflow: 'visible' }}>
{/* Sun */}
{(c === 'clear' || c === 'partly') && (
<g style={{ transformOrigin: c === 'partly' ? '35px 40px' : '50px 50px' }}>
<circle
cx={c === 'partly' ? 35 : 50}
cy={c === 'partly' ? 40 : 50}
r={c === 'partly' ? 14 : 18}
fill="#fbbf24"
style={{ filter: 'drop-shadow(0 0 8px rgba(251,191,36,0.6))' }}
/>
{/* Rays */}
{[0,45,90,135,180,225,270,315].map(angle => (
<line
key={angle}
x1={c === 'partly' ? 35 : 50}
y1={c === 'partly' ? 40 : 50}
x2={c === 'partly' ? 35 + Math.cos(angle * Math.PI / 180) * 22 : 50 + Math.cos(angle * Math.PI / 180) * 28}
y2={c === 'partly' ? 40 + Math.sin(angle * Math.PI / 180) * 22 : 50 + Math.sin(angle * Math.PI / 180) * 28}
stroke="#fbbf24"
strokeWidth={2}
strokeLinecap="round"
opacity={0.6}
style={{
transformOrigin: `${c === 'partly' ? 35 : 50}px ${c === 'partly' ? 40 : 50}px`,
animation: `spin-slow 12s linear infinite`,
}}
/>
))}
</g>
)}
{/* Cloud */}
{(c === 'partly' || c === 'cloudy' || c === 'rain' || c === 'snow' || c === 'thunder') && (
<g style={{ animation: 'cloud-float 4s ease-in-out infinite' }}>
<ellipse cx={58} cy={52} rx={22} ry={14} fill="rgba(200,210,230,0.9)" />
<ellipse cx={45} cy={48} rx={16} ry={12} fill="rgba(210,220,235,0.95)" />
<ellipse cx={68} cy={50} rx={14} ry={10} fill="rgba(195,205,225,0.85)" />
<ellipse cx={52} cy={56} rx={20} ry={10} fill="rgba(205,215,232,0.9)" />
</g>
)}
{/* Rain drops */}
{(c === 'rain' || c === 'thunder') && (
<g>
{[
{ 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) => (
<line
key={i}
x1={drop.x} y1={68} x2={drop.x - 2} y2={76}
stroke="#60a5fa"
strokeWidth={2}
strokeLinecap="round"
opacity={0.7}
style={{
animation: `rain-fall 0.8s ease-in infinite`,
animationDelay: `${drop.delay}s`,
}}
/>
))}
</g>
)}
{/* Thunder bolt */}
{c === 'thunder' && (
<polygon
points="55,55 50,70 56,68 52,82 62,64 56,66 60,55"
fill="#fbbf24"
style={{
animation: 'thunder-flash 2s ease-in-out infinite',
filter: 'drop-shadow(0 0 4px rgba(251,191,36,0.8))',
}}
/>
)}
{/* Snow flakes */}
{c === 'snow' && (
<g>
{[
{ 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) => (
<circle
key={i}
cx={flake.x} cy={72}
r={2.5}
fill="white"
opacity={0.8}
style={{
animation: `snow-fall 2s ease-in-out infinite`,
animationDelay: `${flake.delay}s`,
}}
/>
))}
</g>
)}
{/* Fog lines */}
{c === 'fog' && (
<g>
{[40, 52, 64].map((y, i) => (
<line
key={i}
x1={25} y1={y} x2={75} y2={y}
stroke="rgba(200,210,230,0.5)"
strokeWidth={4}
strokeLinecap="round"
style={{
animation: `fog-drift 3s ease-in-out infinite`,
animationDelay: `${i * 0.5}s`,
}}
/>
))}
</g>
)}
</svg>
</div>
)
}