feat: remove weather from TopBar, clickable forecast days with detail modal (feels like, humidity, wind, precip)
All checks were successful
Deploy / deploy (push) Successful in 3m8s
All checks were successful
Deploy / deploy (push) Successful in 3m8s
This commit is contained in:
@@ -49,7 +49,7 @@ export async function GET(req: Request) {
|
|||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon,
|
longitude: lon,
|
||||||
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
||||||
daily: "weather_code,temperature_2m_max,temperature_2m_min",
|
daily: "weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_probability_max,wind_speed_10m_max,relative_humidity_2m_max",
|
||||||
timezone: "Europe/Moscow",
|
timezone: "Europe/Moscow",
|
||||||
forecast_days: "7",
|
forecast_days: "7",
|
||||||
});
|
});
|
||||||
@@ -73,6 +73,11 @@ export async function GET(req: Request) {
|
|||||||
minTemp: String(Math.round(daily.temperature_2m_min[i])),
|
minTemp: String(Math.round(daily.temperature_2m_min[i])),
|
||||||
desc: wmoToDesc(daily.weather_code[i]),
|
desc: wmoToDesc(daily.weather_code[i]),
|
||||||
weatherCode: wmoToWttrCode(daily.weather_code[i]),
|
weatherCode: wmoToWttrCode(daily.weather_code[i]),
|
||||||
|
feelsLikeMax: String(Math.round(daily.apparent_temperature_max?.[i] ?? 0)),
|
||||||
|
feelsLikeMin: String(Math.round(daily.apparent_temperature_min?.[i] ?? 0)),
|
||||||
|
precipProb: String(daily.precipitation_probability_max?.[i] ?? 0),
|
||||||
|
windSpeed: String(Math.round((daily.wind_speed_10m_max?.[i] ?? 0) / 3.6)),
|
||||||
|
humidity: String(daily.relative_humidity_2m_max?.[i] ?? 0),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
95
app/page.tsx
95
app/page.tsx
@@ -19,7 +19,7 @@ interface WeatherData {
|
|||||||
humidity: string
|
humidity: string
|
||||||
windSpeed: string
|
windSpeed: string
|
||||||
feelsLike: string
|
feelsLike: string
|
||||||
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string }[]
|
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string; feelsLikeMax?: string; feelsLikeMin?: string; precipProb?: string; windSpeed?: string; humidity?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SensorData {
|
interface SensorData {
|
||||||
@@ -285,12 +285,91 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ————— Home Tab —————
|
// ————— Home Tab —————
|
||||||
|
// ————— Weather Day Detail Modal —————
|
||||||
|
function WeatherDayModal({ day, current, onClose }: {
|
||||||
|
day: { date: string; maxTemp: string; minTemp: string; desc: string; feelsLikeMax?: string; feelsLikeMin?: string; precipProb?: string; windSpeed?: string; humidity?: string }
|
||||||
|
current: WeatherData | null
|
||||||
|
onClose: () => void
|
||||||
|
}) {
|
||||||
|
const d = new Date(day.date)
|
||||||
|
const isToday = d.toDateString() === new Date().toDateString()
|
||||||
|
const dayLabel = isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(12px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }} onClick={onClose}>
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(16,16,30,0.97)', backdropFilter: 'blur(40px)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.07)', borderRadius: 28,
|
||||||
|
width: 400, maxWidth: '90vw', overflow: 'hidden',
|
||||||
|
boxShadow: '0 30px 90px rgba(0,0,0,0.6)',
|
||||||
|
}} onClick={e => e.stopPropagation()}>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<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: '28px 28px 24px', textAlign: 'center',
|
||||||
|
position: 'relative', overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{ position: 'absolute', top: -10, right: 10, opacity: 0.1, pointerEvents: 'none' }}>
|
||||||
|
<WeatherAnimation condition={day.desc} size={100} />
|
||||||
|
</div>
|
||||||
|
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||||
|
<div style={{ fontSize: 14, color: 'var(--text-secondary)', fontWeight: 500, textTransform: 'capitalize', marginBottom: 12 }}>{dayLabel}</div>
|
||||||
|
<WeatherAnimation condition={day.desc} size={64} />
|
||||||
|
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'center', gap: 8, marginTop: 12 }}>
|
||||||
|
<span style={{ fontSize: 42, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-2px' }}>{day.maxTemp}°</span>
|
||||||
|
<span style={{ fontSize: 22, fontWeight: 500, color: 'var(--text-secondary)' }}>{day.minTemp}°</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 15, color: 'var(--text-secondary)', marginTop: 6, fontWeight: 500 }}>{day.desc}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details grid */}
|
||||||
|
<div style={{ padding: '22px 28px 28px' }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 10 }}>
|
||||||
|
{[
|
||||||
|
{ icon: <Thermometer size={18} color="#fb923c" />, bg: 'rgba(251,146,60,0.1)', label: 'Ощущается', value: `${day.feelsLikeMax || '—'}° / ${day.feelsLikeMin || '—'}°` },
|
||||||
|
{ icon: <Droplets size={18} color="#3b82f6" />, bg: 'rgba(59,130,246,0.1)', label: 'Влажность', value: `${day.humidity || (isToday && current ? current.humidity : '—')}%` },
|
||||||
|
{ icon: <Wind size={18} color="#22d3ee" />, bg: 'rgba(34,211,238,0.1)', label: 'Ветер', value: `${day.windSpeed || (isToday && current ? current.windSpeed : '—')} м/с` },
|
||||||
|
{ icon: <span style={{ fontSize: 18 }}>🌧️</span>, bg: 'rgba(99,102,241,0.1)', label: 'Вероятность осадков', value: `${day.precipProb || '0'}%` },
|
||||||
|
].map(item => (
|
||||||
|
<div key={item.label} style={{
|
||||||
|
padding: '16px 14px', borderRadius: 16,
|
||||||
|
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)',
|
||||||
|
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: 38, height: 38, borderRadius: 12, background: item.bg,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>{item.icon}</div>
|
||||||
|
<div style={{ fontSize: 17, fontWeight: 700, color: 'var(--text-primary)' }}>{item.value}</div>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textAlign: 'center' }}>{item.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={onClose} style={{
|
||||||
|
width: '100%', padding: '13px', borderRadius: 14, marginTop: 16,
|
||||||
|
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
color: 'var(--text-secondary)', fontSize: 14, fontWeight: 600,
|
||||||
|
}}>
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) {
|
function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) {
|
||||||
const [todayEvents, setTodayEvents] = useState<CalendarEvent[]>([])
|
const [todayEvents, setTodayEvents] = useState<CalendarEvent[]>([])
|
||||||
const [tomorrowEvents, setTomorrowEvents] = useState<CalendarEvent[]>([])
|
const [tomorrowEvents, setTomorrowEvents] = useState<CalendarEvent[]>([])
|
||||||
const [calLoading, setCalLoading] = useState(true)
|
const [calLoading, setCalLoading] = useState(true)
|
||||||
const [greeting, setGreeting] = useState(getGreeting())
|
const [greeting, setGreeting] = useState(getGreeting())
|
||||||
const [pinnedNotes, setPinnedNotes] = useState<any[]>([])
|
const [pinnedNotes, setPinnedNotes] = useState<any[]>([])
|
||||||
|
const [selectedDay, setSelectedDay] = useState<any>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/calendar?range=today')
|
fetch('/api/calendar?range=today')
|
||||||
@@ -376,7 +455,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current */}
|
{/* Current */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1 }}>
|
<div onClick={() => weather?.forecast?.[0] && setSelectedDay(weather.forecast[0])} style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1, cursor: 'pointer' }}>
|
||||||
<WeatherAnimation condition={weather.desc} size={48} />
|
<WeatherAnimation condition={weather.desc} size={48} />
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 32, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
|
<div style={{ fontSize: 32, fontWeight: 800, color: 'var(--text-primary)', lineHeight: 1, letterSpacing: '-2px' }}>{weather.temp}°</div>
|
||||||
@@ -394,10 +473,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
|||||||
const d = new Date(day.date)
|
const d = new Date(day.date)
|
||||||
const isToday = idx === 0
|
const isToday = idx === 0
|
||||||
return (
|
return (
|
||||||
<div key={day.date} style={{
|
<div key={day.date} onClick={() => setSelectedDay(day)} style={{
|
||||||
flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px',
|
flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px',
|
||||||
borderRadius: 10,
|
borderRadius: 10, cursor: 'pointer',
|
||||||
background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent',
|
background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent',
|
||||||
|
transition: 'background 0.2s ease',
|
||||||
}}>
|
}}>
|
||||||
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', fontWeight: 600, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
<div style={{ fontSize: 9, color: isToday ? '#a5b4fc' : 'var(--text-secondary)', fontWeight: 600, marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
{isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).slice(0, 2)}
|
{isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).slice(0, 2)}
|
||||||
@@ -523,6 +603,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Weather day detail modal */}
|
||||||
|
{selectedDay && (
|
||||||
|
<WeatherDayModal day={selectedDay} current={weather} onClose={() => setSelectedDay(null)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -848,7 +933,7 @@ function HomePageInner() {
|
|||||||
<Sidebar active={tab} onChange={setTab} />
|
<Sidebar active={tab} onChange={setTab} />
|
||||||
|
|
||||||
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0, position: 'relative', zIndex: 1 }}>
|
<main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0, position: 'relative', zIndex: 1 }}>
|
||||||
<TopBar weather={weather} sensors={sensors} haConnected={haConnected} />
|
<TopBar sensors={sensors} haConnected={haConnected} />
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{tab === 'home' && (
|
{tab === 'home' && (
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Droplets, Wind, Thermometer, X } from 'lucide-react'
|
import { Droplets, Wind, Thermometer } 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 {
|
interface SensorData {
|
||||||
temperature: number
|
temperature: number
|
||||||
@@ -20,23 +10,10 @@ interface SensorData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TopBarProps {
|
interface TopBarProps {
|
||||||
weather: WeatherData | null
|
|
||||||
sensors: SensorData | null
|
sensors: SensorData | null
|
||||||
haConnected?: boolean
|
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 {
|
function formatTime(date: Date): string {
|
||||||
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
|
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
|
||||||
@@ -49,27 +26,15 @@ function formatDate(date: Date): string {
|
|||||||
return `${weekday}, ${day} ${month}`
|
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) {
|
export default function TopBar({ sensors, haConnected }: TopBarProps) {
|
||||||
const [time, setTime] = useState(() => new Date())
|
const [time, setTime] = useState(() => new Date())
|
||||||
const [showModal, setShowModal] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = setInterval(() => setTime(new Date()), 1000)
|
const t = setInterval(() => setTime(new Date()), 1000)
|
||||||
return () => clearInterval(t)
|
return () => clearInterval(t)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const windMs = weather ? parseInt(weather.windSpeed) || 0 : 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
@@ -134,183 +99,9 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) {
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
</header>
|
</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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user