From bce9578fa1ad20f987bd9d31ba246982e9fd4dc2 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Wed, 22 Apr 2026 20:58:05 +0000 Subject: [PATCH] feat: redesigned Home (weather+forecast bar, today+tomorrow, pinned notes), fix snow animation, scrollable weather modal, weather hints --- app/api/notes/route.ts | 2 + app/globals.css | 8 +- app/page.tsx | 294 ++++++++++++++++++++++---------- components/TopBar.tsx | 3 +- components/WeatherAnimation.tsx | 31 ++-- 5 files changed, 229 insertions(+), 109 deletions(-) diff --git a/app/api/notes/route.ts b/app/api/notes/route.ts index a070752..04d08e4 100644 --- a/app/api/notes/route.ts +++ b/app/api/notes/route.ts @@ -11,6 +11,7 @@ interface Note { items?: { id: string; text: string; done: boolean }[] text?: string color: string + pinDate: string | null createdAt: string updatedAt: string } @@ -42,6 +43,7 @@ export async function POST(req: Request) { items: body.type === 'shopping' ? [] : undefined, text: body.type === 'note' ? '' : undefined, color: body.color || '#6366f1', + pinDate: body.pinDate || null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), } diff --git a/app/globals.css b/app/globals.css index a3df93a..f3ab2d9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -238,9 +238,11 @@ button:focus-visible { } @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; } + 0% { transform: translateY(0) translateX(0); opacity: 0.9; } + 25% { transform: translateY(5px) translateX(2px); opacity: 0.8; } + 50% { transform: translateY(10px) translateX(-1px); opacity: 0.7; } + 75% { transform: translateY(15px) translateX(3px); opacity: 0.4; } + 100% { transform: translateY(22px) translateX(0); opacity: 0; } } @keyframes thunder-flash { diff --git a/app/page.tsx b/app/page.tsx index 16c7397..42e9abc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete, KeyRound, MapPin, Info, Check, X as XIcon } from 'lucide-react' +import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete, KeyRound, MapPin, Info, Check, X as XIcon, StickyNote, ShoppingCart, FileText } from 'lucide-react' import Sidebar from '@/components/Sidebar' import TopBar from '@/components/TopBar' import RoomTabs from '@/components/RoomTabs' @@ -287,8 +287,10 @@ function LockScreen({ onUnlock }: { onUnlock: () => void }) { // ————— Home Tab ————— 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([]) useEffect(() => { fetch('/api/calendar?range=today') @@ -296,6 +298,28 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S .then(d => setTodayEvents(d.events || [])) .catch(() => setTodayEvents([])) .finally(() => setCalLoading(false)) + + // Tomorrow + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + const y = tomorrow.getFullYear() + const m = tomorrow.getMonth() + fetch(`/api/calendar?range=month&year=${y}&month=${m}`) + .then(r => r.json()) + .then(d => { + const tmr = (d.events || []).filter((e: any) => { + const ed = new Date(e.start) + return ed.getDate() === tomorrow.getDate() && ed.getMonth() === tomorrow.getMonth() + }) + setTomorrowEvents(tmr) + }) + .catch(() => {}) + + // Notes + fetch('/api/notes') + .then(r => r.json()) + .then(d => setPinnedNotes((d.notes || []).slice(0, 3))) + .catch(() => {}) }, []) useEffect(() => { @@ -303,115 +327,207 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S return () => clearInterval(t) }, []) - const pm25Info = sensors ? getPm25Level(sensors.pm25) : null + // Weather hint + const getWeatherHint = (): string | null => { + if (!weather) return null + const desc = weather.desc.toLowerCase() + const temp = parseInt(weather.temp) + if (desc.includes('дождь') || desc.includes('ливен') || desc.includes('морос')) return '☂️ Не забудьте зонт' + if (desc.includes('снег')) return '🧤 На улице снег, одевайтесь теплее' + if (desc.includes('гроз')) return '⛈️ Ожидается гроза' + if (temp <= 0) return '🥶 На улице мороз' + if (temp <= 5) return '🧥 Оденьтесь потеплее' + if (temp >= 30) return '🥵 Очень жарко, пейте воду' + return null + } + + const hint = getWeatherHint() return ( -
- {/* Greeting */} -
-

+
+ {/* Greeting + hint */} +
+

{greeting} 👋

-

- Вот что происходит дома -

+ {hint && ( +
+ {hint} +
+ )}
-
- {weather && ( -
-
{getWeatherIcon(weather.desc)}
-
Погода
-
- -
-
{weather.temp}°
-
{weather.desc}
-
+ {/* Weather — full width compact */} + {weather && ( +
+
+ +
+ + {/* Current */} +
+ +
+
{weather.temp}°
+
{weather.desc}
- {weather.forecast && weather.forecast.length > 0 && ( -
- {weather.forecast.map((day, idx) => { - const d = new Date(day.date) - const isToday = idx === 0 - return ( -
-
- {isToday ? 'Сегодня' : d.toLocaleDateString('ru-RU', { weekday: 'short' })} -
-
{getWeatherIcon(day.desc)}
-
{day.maxTemp}°
-
{day.minTemp}°
+
+ + {/* Divider */} +
+ + {/* 7 day forecast */} + {weather.forecast && ( +
+ {weather.forecast.map((day, idx) => { + const d = new Date(day.date) + const isToday = idx === 0 + return ( +
+
+ {isToday ? 'Сей' : d.toLocaleDateString('ru-RU', { weekday: 'short' }).slice(0, 2)}
- ) - })} +
{getWeatherIcon(day.desc)}
+
{day.maxTemp}°
+
{day.minTemp}°
+
+ ) + })} +
+ )} +
+ )} + + {/* Two columns: Events + Notes */} +
+ + {/* Left: Today + Tomorrow events */} +
+ {/* Today */} +
+
+ + Сегодня + {todayEvents.length} +
+ {calLoading ? ( +
Загрузка...
+ ) : todayEvents.length === 0 ? ( +
Свободный день
+ ) : ( +
+ {todayEvents.map(ev => ( +
+
+
+
{ev.title}
+
+ {ev.allDay ? 'Весь день' : formatEventTime(ev.start)} · {ev.ownerName} +
+
+
+ ))}
)}
- )} - {sensors && ( -
-
Климат в квартире
-
-
-
-
{sensors.temperature}°C
Температура
+ + {/* Tomorrow */} +
+
+ + Завтра + {tomorrowEvents.length} +
+ {tomorrowEvents.length === 0 ? ( +
Нет событий
+ ) : ( +
+ {tomorrowEvents.map(ev => ( +
+
+
+
{ev.title}
+
+ {ev.allDay ? 'Весь день' : formatEventTime(ev.start)} +
+
+
+ ))}
-
-
-
{sensors.humidity}%
Влажность
-
-
-
-
-
{sensors.pm25}µg/m³
-
PM2.5 · {pm25Info?.label}
-
+ )} +
+
+ + {/* Right: Pinned notes / shopping lists */} +
+ {pinnedNotes.length === 0 ? ( +
+
+ +
Заметки появятся здесь
-
- )} -
- -
-
- - Сегодня -
- {calLoading ? ( -
Загрузка...
- ) : todayEvents.length === 0 ? ( -
Нет событий на сегодня
- ) : ( -
- {todayEvents.map(ev => ( -
-
-
-
{ev.title}
-
- {ev.allDay ? 'Весь день' : `${formatEventTime(ev.start)} — ${formatEventTime(ev.end)}`} - {ev.ownerName} + ) : ( + pinnedNotes.map(note => { + const doneCount = note.items?.filter((i: any) => i.done).length || 0 + const totalCount = note.items?.length || 0 + return ( +
+
+ {note.type === 'shopping' ? : } + {note.title} + {note.type === 'shopping' && totalCount > 0 && ( + {doneCount}/{totalCount} + )}
+ {note.type === 'shopping' ? ( +
+ {(note.items || []).filter((i: any) => !i.done).slice(0, 5).map((item: any) => ( +
+
+ {item.text} +
+ ))} + {(note.items || []).filter((i: any) => !i.done).length > 5 && ( +
+ +{(note.items || []).filter((i: any) => !i.done).length - 5} ещё +
+ )} +
+ ) : ( +
+ {note.text || 'Пустая заметка'} +
+ )}
-
- ))} -
- )} + ) + }) + )} +
) } + // ————— Settings Tab ————— function SettingsTab({ city, onCityChange, onLogout, theme, onThemeChange }: { city: string; onCityChange: (id: string) => void; onLogout: () => void; theme: string; onThemeChange: (t: string) => void }) { const [showPinChange, setShowPinChange] = useState(false) diff --git a/components/TopBar.tsx b/components/TopBar.tsx index d4559c1..589e399 100644 --- a/components/TopBar.tsx +++ b/components/TopBar.tsx @@ -161,6 +161,7 @@ export default function TopBar({ weather, sensors, haConnected }: TopBarProps) { 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, }} >
diff --git a/components/WeatherAnimation.tsx b/components/WeatherAnimation.tsx index 60cc55b..5aab05a 100644 --- a/components/WeatherAnimation.tsx +++ b/components/WeatherAnimation.tsx @@ -106,23 +106,22 @@ export default function WeatherAnimation({ condition, size = 64 }: WeatherAnimat {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 }, + { x: 44, delay: 0, size: 3 }, + { x: 54, delay: 0.5, size: 2.5 }, + { x: 64, delay: 1.0, size: 2 }, + { x: 49, delay: 1.5, size: 2.5 }, + { x: 59, delay: 0.3, size: 3 }, ].map((flake, i) => ( - + + + ))} )}