From 3a93d5bbea31617f81b4af8c23b6b83b07f12e40 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Wed, 22 Apr 2026 21:00:37 +0000 Subject: [PATCH] feat: remove weather from TopBar, clickable forecast days with detail modal (feels like, humidity, wind, precip) --- app/api/weather/route.ts | 7 +- app/page.tsx | 95 ++++++++++++++++- components/TopBar.tsx | 213 +-------------------------------------- 3 files changed, 98 insertions(+), 217 deletions(-) diff --git a/app/api/weather/route.ts b/app/api/weather/route.ts index 8b8477b..3ba0a41 100644 --- a/app/api/weather/route.ts +++ b/app/api/weather/route.ts @@ -49,7 +49,7 @@ export async function GET(req: Request) { latitude: lat, longitude: lon, 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", forecast_days: "7", }); @@ -73,6 +73,11 @@ export async function GET(req: Request) { minTemp: String(Math.round(daily.temperature_2m_min[i])), desc: wmoToDesc(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({ diff --git a/app/page.tsx b/app/page.tsx index 42e9abc..ab1fd13 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -19,7 +19,7 @@ interface WeatherData { humidity: string windSpeed: 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 { @@ -285,12 +285,91 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) { } // ————— 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 ( +
+
e.stopPropagation()}> + + {/* Hero */} +
+
+ +
+
+
{dayLabel}
+ +
+ {day.maxTemp}° + {day.minTemp}° +
+
{day.desc}
+
+
+ + {/* Details grid */} +
+
+ {[ + { icon: , bg: 'rgba(251,146,60,0.1)', label: 'Ощущается', value: `${day.feelsLikeMax || '—'}° / ${day.feelsLikeMin || '—'}°` }, + { icon: , bg: 'rgba(59,130,246,0.1)', label: 'Влажность', value: `${day.humidity || (isToday && current ? current.humidity : '—')}%` }, + { icon: , bg: 'rgba(34,211,238,0.1)', label: 'Ветер', value: `${day.windSpeed || (isToday && current ? current.windSpeed : '—')} м/с` }, + { icon: 🌧️, bg: 'rgba(99,102,241,0.1)', label: 'Вероятность осадков', value: `${day.precipProb || '0'}%` }, + ].map(item => ( +
+
{item.icon}
+
{item.value}
+
{item.label}
+
+ ))} +
+ + +
+
+
+ ) +} + + function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) { const [todayEvents, setTodayEvents] = useState([]) const [tomorrowEvents, setTomorrowEvents] = useState([]) const [calLoading, setCalLoading] = useState(true) const [greeting, setGreeting] = useState(getGreeting()) const [pinnedNotes, setPinnedNotes] = useState([]) + const [selectedDay, setSelectedDay] = useState(null) useEffect(() => { fetch('/api/calendar?range=today') @@ -376,7 +455,7 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S {/* Current */} -
+
weather?.forecast?.[0] && setSelectedDay(weather.forecast[0])} style={{ display: 'flex', alignItems: 'center', gap: 14, flexShrink: 0, position: 'relative', zIndex: 1, cursor: 'pointer' }}>
{weather.temp}°
@@ -394,10 +473,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S const d = new Date(day.date) const isToday = idx === 0 return ( -
setSelectedDay(day)} style={{ flex: 1, minWidth: 0, textAlign: 'center', padding: '4px 2px', - borderRadius: 10, + borderRadius: 10, cursor: 'pointer', background: isToday ? 'rgba(99,102,241,0.1)' : 'transparent', + transition: 'background 0.2s ease', }}>
{isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).slice(0, 2)} @@ -523,6 +603,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S )}
+ + {/* Weather day detail modal */} + {selectedDay && ( + setSelectedDay(null)} /> + )}
) } @@ -848,7 +933,7 @@ function HomePageInner() {
- + {tab === 'home' && ( diff --git a/components/TopBar.tsx b/components/TopBar.tsx index 589e399..3a8ef8f 100644 --- a/components/TopBar.tsx +++ b/components/TopBar.tsx @@ -1,17 +1,7 @@ 'use client' import { useState, useEffect } from 'react' -import { Droplets, Wind, Thermometer, X } 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 }[] -} +import { Droplets, Wind, Thermometer } from 'lucide-react' interface SensorData { temperature: number @@ -20,23 +10,10 @@ interface SensorData { } interface TopBarProps { - weather: WeatherData | null sensors: SensorData | null 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 { return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) @@ -49,27 +26,15 @@ function formatDate(date: Date): string { 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 [showModal, setShowModal] = useState(false) useEffect(() => { const t = setInterval(() => setTime(new Date()), 1000) return () => clearInterval(t) }, []) - const windMs = weather ? parseInt(weather.windSpeed) || 0 : 0 - return ( <>
)} - {weather && ( - - )}
- {/* Weather Modal */} - {showModal && weather && ( -
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, - }} - > -
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 */} -
- {/* Background emoji */} -
- -
- -
-
- -
-
- {weather.temp}° -
-
- {weather.desc} -
-
-
- -
-
- - {/* Details */} -
- - {/* Stats grid */} -
- {[ - { - icon: , - bg: 'rgba(251,146,60,0.1)', - label: 'Ощущается', - value: `${weather.feelsLike}°`, - }, - { - icon: , - bg: 'rgba(59,130,246,0.1)', - label: 'Влажность', - value: `${weather.humidity}%`, - }, - { - icon: , - bg: 'rgba(34,211,238,0.1)', - label: getWindDesc(windMs), - value: `${weather.windSpeed} м/с`, - }, - ].map(item => ( -
-
- {item.icon} -
-
{item.value}
-
{item.label}
-
- ))} -
- - {/* Forecast */} - {weather.forecast && weather.forecast.length > 0 && ( - <> -
- Прогноз на неделю -
-
- {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 ( -
- {/* Icon */} - - - {/* Day info */} -
-
- {isToday ? 'Сегодня' : weekday} -
-
- {dateStr} · {day.desc} -
-
- - {/* Temps */} -
- - {day.maxTemp}° - - - {day.minTemp}° - -
-
- ) - })} -
- - )} -
-
-
- )} ) }