From eb644ff3412b2392e8d0d131b696fb8e0db2472a Mon Sep 17 00:00:00 2001 From: Cosmo Date: Wed, 22 Apr 2026 18:38:31 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20premium=20UI=20redesign=20=E2=80=94=20g?= =?UTF-8?q?lassmorphism,=20gradient=20accents,=20ambient=20background?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/globals.css | 123 +++++++++++-- app/layout.tsx | 3 +- app/page.tsx | 352 +++++++++++++++++++++---------------- components/CalendarTab.tsx | 268 ++++++++++++++++++---------- components/DeviceCard.tsx | 115 ++++++++---- components/RoomTabs.tsx | 58 +++--- components/Sidebar.tsx | 66 ++++--- components/TopBar.tsx | 214 +++++++++++----------- 8 files changed, 763 insertions(+), 436 deletions(-) diff --git a/app/globals.css b/app/globals.css index a2aa989..65474a5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,22 +5,37 @@ } :root { - --bg: #0a0a14; - --sidebar-bg: rgba(255, 255, 255, 0.02); - --card-bg: rgba(255, 255, 255, 0.05); - --card-border: rgba(255, 255, 255, 0.08); - --text-primary: rgba(255, 255, 255, 0.92); + --bg: #0c0c18; + --bg-secondary: #12121f; + --sidebar-bg: rgba(12, 12, 24, 0.8); + --card-bg: rgba(255, 255, 255, 0.04); + --card-bg-hover: rgba(255, 255, 255, 0.07); + --card-border: rgba(255, 255, 255, 0.07); + --card-border-hover: rgba(255, 255, 255, 0.12); + --text-primary: rgba(255, 255, 255, 0.95); --text-secondary: rgba(255, 255, 255, 0.45); - --accent: #00d4ff; - --accent-glow: rgba(0, 212, 255, 0.15); - --on-color: #00d4ff; - --off-color: rgba(255, 255, 255, 0.2); + --text-tertiary: rgba(255, 255, 255, 0.25); + --accent: #818cf8; + --accent-secondary: #22d3ee; + --accent-glow: rgba(129, 140, 248, 0.15); + --glass: rgba(255, 255, 255, 0.03); + --glass-border: rgba(255, 255, 255, 0.06); + --gradient-primary: linear-gradient(135deg, #6366f1, #8b5cf6); + --gradient-warm: linear-gradient(135deg, #f59e0b, #ef4444); + --gradient-cool: linear-gradient(135deg, #06b6d4, #3b82f6); + --gradient-green: linear-gradient(135deg, #10b981, #34d399); + --on-color: #818cf8; + --off-color: rgba(255, 255, 255, 0.15); + --radius-sm: 12px; + --radius-md: 16px; + --radius-lg: 22px; + --radius-xl: 28px; } html, body { background: var(--bg); color: var(--text-primary); - font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; height: 100%; overflow: hidden; -webkit-font-smoothing: antialiased; @@ -31,6 +46,70 @@ html, body { height: 100%; } +/* Ambient background orbs */ +.bg-ambient { + position: fixed; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; +} +.bg-ambient::before { + content: ''; + position: absolute; + width: 600px; + height: 600px; + border-radius: 50%; + background: radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%); + top: -200px; + right: -100px; + animation: float1 20s ease-in-out infinite; +} +.bg-ambient::after { + content: ''; + position: absolute; + width: 500px; + height: 500px; + border-radius: 50%; + background: radial-gradient(circle, rgba(139, 92, 246, 0.06) 0%, transparent 70%); + bottom: -150px; + left: -50px; + animation: float2 25s ease-in-out infinite; +} + +@keyframes float1 { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(30px, 40px) scale(1.05); } + 66% { transform: translate(-20px, 20px) scale(0.95); } +} +@keyframes float2 { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(-40px, -30px) scale(1.1); } + 66% { transform: translate(20px, -10px) scale(0.9); } +} + +/* Glass card base */ +.glass-card { + background: var(--card-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--card-border); + border-radius: var(--radius-lg); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} +.glass-card:hover { + background: var(--card-bg-hover); + border-color: var(--card-border-hover); +} + +/* Gradient text */ +.gradient-text { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + button { cursor: pointer; border: none; @@ -46,20 +125,34 @@ button:focus-visible { outline-offset: 2px; } +/* Smooth scrollbar */ ::-webkit-scrollbar { width: 4px; height: 4px; } - ::-webkit-scrollbar-track { background: transparent; } - ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.08); border-radius: 2px; } - ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.15); +} + +/* Pulse animation for active devices */ +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 20px rgba(129, 140, 248, 0.15); } + 50% { box-shadow: 0 0 30px rgba(129, 140, 248, 0.25); } +} + +/* Slide in animation */ +@keyframes slideUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-slide-up { + animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; } diff --git a/app/layout.tsx b/app/layout.tsx index 3fe4599..0d44276 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,7 +3,7 @@ import "./globals.css"; export const metadata: Metadata = { title: "Smart Home Dashboard", - description: "Smart Home Tablet Dashboard — управление умным домом", + description: "Smart Home Tablet Dashboard", manifest: "/manifest.json", }; @@ -26,6 +26,7 @@ export default function RootLayout({ + {children} diff --git a/app/page.tsx b/app/page.tsx index 54986db..547a321 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useCallback } from 'react' +import { Thermometer, Droplets, Wind, Calendar, Sun, CloudRain, Snowflake as SnowIcon, Cloud, CloudSun, Zap, Settings as SettingsIcon } from 'lucide-react' import Sidebar from '@/components/Sidebar' import TopBar from '@/components/TopBar' import RoomTabs from '@/components/RoomTabs' @@ -56,53 +57,19 @@ const DEVICES_BY_ROOM: Record = { living: [ - { - id: 'air_purifier', - name: 'Очиститель воздуха', - icon: '💨', - entityId: 'fan.zhimi_rmb1_9528_air_purifier', - domain: 'fan', - haKey: 'fan.air_purifier', - isMock: false, - }, - { - id: 'light_living', - name: 'Свет', - icon: '💡', - entityId: 'light.living_room', - domain: 'light', - haKey: 'light.living_room', - isMock: true, - }, - { - id: 'tv', - name: 'Телевизор', - icon: '📺', - isMock: true, - }, + { id: 'air_purifier', name: 'Очиститель воздуха', icon: '💨', entityId: 'fan.zhimi_rmb1_9528_air_purifier', domain: 'fan', haKey: 'fan.air_purifier', isMock: false }, + { id: 'light_living', name: 'Свет', icon: '💡', entityId: 'light.living_room', domain: 'light', haKey: 'light.living_room', isMock: true }, + { id: 'tv', name: 'Телевизор', icon: '📺', isMock: true }, ], bedroom: [ - { - id: 'light_bedroom', - name: 'Свет', - icon: '💡', - entityId: 'light.bedroom', - domain: 'light', - haKey: 'light.bedroom', - isMock: true, - }, - { - id: 'ac', - name: 'Кондиционер', - icon: '❄️', - isMock: true, - }, + { id: 'light_bedroom', name: 'Свет', icon: '💡', entityId: 'light.bedroom', domain: 'light', haKey: 'light.bedroom', isMock: true }, + { id: 'ac', name: 'Кондиционер', icon: '❄️', isMock: true }, ], kitchen: [], bathroom: [], } -function getWeatherEmoji(desc: string): string { +function getWeatherIcon(desc: string): string { const d = desc?.toLowerCase() || '' if (d.includes('ясно') || d.includes('солнеч')) return '☀️' if (d.includes('облач')) return '⛅' @@ -118,6 +85,13 @@ function formatEventTime(iso: string): string { return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) } +function getPm25Level(pm25: number): { label: string; color: string; bg: string } { + if (pm25 <= 12) return { label: 'Отлично', color: '#34d399', bg: 'rgba(52,211,153,0.12)' } + if (pm25 <= 35) return { label: 'Хорошо', color: '#a3e635', bg: 'rgba(163,230,53,0.12)' } + if (pm25 <= 55) return { label: 'Умеренно', color: '#fbbf24', bg: 'rgba(251,191,36,0.12)' } + return { label: 'Плохо', color: '#f87171', bg: 'rgba(248,113,113,0.12)' } +} + // ————— Home Tab ————— function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) { const [todayEvents, setTodayEvents] = useState([]) @@ -131,38 +105,182 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S .finally(() => setCalLoading(false)) }, []) - return ( -
+ const pm25Info = sensors ? getPm25Level(sensors.pm25) : null - {/* Today Widget */} + return ( +
+ + {/* Top row: Weather + Sensors side by side */} +
+ + {/* Weather Card */} + {weather && ( +
+ {/* Background decoration */} +
+ {getWeatherIcon(weather.desc)} +
+ +
+ Погода +
+ +
+ {getWeatherIcon(weather.desc)} +
+
{weather.temp}°
+
{weather.desc}
+
+
+ + {/* Forecast mini */} + {weather.forecast && weather.forecast.length > 0 && ( +
+ {weather.forecast.slice(0, 3).map(day => { + const d = new Date(day.date) + const label = d.toLocaleDateString('ru-RU', { weekday: 'short' }) + return ( +
+
{label}
+
{getWeatherIcon(day.desc)}
+
{day.maxTemp}°
+
{day.minTemp}°
+
+ ) + })} +
+ )} +
+ )} + + {/* Sensors Card */} + {sensors && ( +
+
+ Климат в квартире +
+
+ {/* Temperature */} +
+
+ +
+
+
{sensors.temperature}°C
+
Температура
+
+
+ + {/* Humidity */} +
+
+ +
+
+
{sensors.humidity}%
+
Влажность
+
+
+ + {/* PM2.5 */} +
+
+ +
+
+
+ {sensors.pm25} + µg/m³ +
+
PM2.5 · {pm25Info?.label}
+
+
+
+
+ )} +
+ + {/* Today Events */}
-
- 📅 Сегодня +
+ + + Сегодня +
{calLoading ? (
Загрузка...
) : todayEvents.length === 0 ? ( -
- Свободный день 🎉 +
+ Нет событий на сегодня
) : (
{todayEvents.map(ev => ( -
-
+
+
{ev.title}
-
+
{ev.allDay ? 'Весь день' : `${formatEventTime(ev.start)} — ${formatEventTime(ev.end)}`} - {ev.ownerName} + {ev.ownerName}
@@ -170,90 +288,6 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
)}
- - {/* Weather Widget */} - {weather && ( -
-
- 🌤️ Погода -
- - {/* Current */} -
- {getWeatherEmoji(weather.desc)} -
-
{weather.temp}°C
-
{weather.desc}
-
- 💧 {weather.humidity}% · 💨 {weather.windSpeed} км/ч · Ощущается {weather.feelsLike}° -
-
-
- - {/* Forecast */} - {weather.forecast && weather.forecast.length > 0 && ( -
- {weather.forecast.slice(0, 3).map(day => { - const d = new Date(day.date) - const label = d.toLocaleDateString('ru-RU', { weekday: 'short' }) - return ( -
-
{label}
-
{getWeatherEmoji(day.desc)}
-
{day.maxTemp}°
-
{day.minTemp}°
-
- ) - })} -
- )} -
- )} - - {/* Sensors Widget */} - {sensors && ( -
-
- 📊 Датчики квартиры -
-
- {[ - { label: 'Температура', value: `${sensors.temperature}°C`, icon: '🌡️' }, - { label: 'Влажность', value: `${sensors.humidity}%`, icon: '💧' }, - { label: 'PM2.5', value: `${sensors.pm25} μg`, icon: '💨' }, - ].map(s => ( -
-
{s.icon}
-
{s.value}
-
{s.label}
-
- ))} -
-
- )}
) } @@ -301,9 +335,7 @@ export default function HomePage() { } const getDeviceExtra = (id: string): string | undefined => { - if (id === 'air_purifier' && sensors) { - return `PM2.5: ${sensors.pm25}` - } + if (id === 'air_purifier' && sensors) return `PM2.5: ${sensors.pm25}` return undefined } @@ -314,7 +346,11 @@ export default function HomePage() { width: '100%', background: 'var(--bg)', overflow: 'hidden', + position: 'relative', }}> + {/* Ambient background */} +
+
@@ -335,7 +373,7 @@ export default function HomePage() { flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, - padding: '16px 20px 24px', + padding: '16px 24px 28px', }}> {devicesInRoom.length === 0 ? (
- 🏠 - Устройства не добавлены +
🏠
+ Устройства не добавлены
) : ( -
+
{devicesInRoom.map(device => ( - ⚙️ - Настройки +
+ +
+ Настройки Скоро
)} diff --git a/components/CalendarTab.tsx b/components/CalendarTab.tsx index b5b3123..969fea7 100644 --- a/components/CalendarTab.tsx +++ b/components/CalendarTab.tsx @@ -27,18 +27,24 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string; const [saving, setSaving] = useState(false) const [error, setError] = useState('') + const inputStyle = { + padding: '12px 16px', + borderRadius: 14, + background: 'rgba(255,255,255,0.05)', + border: '1px solid rgba(255,255,255,0.08)', + color: 'var(--text-primary)', + fontSize: 14, + outline: 'none', + fontFamily: 'inherit', + transition: 'border-color 0.2s ease', + } + const save = async () => { if (!title.trim()) { setError('Введите название'); return } setSaving(true) setError('') try { - const body = { - title: title.trim(), - date, - startTime: allDay ? null : startTime, - endTime: allDay ? null : endTime, - allDay, - } + const body = { title: title.trim(), date, startTime: allDay ? null : startTime, endTime: allDay ? null : endTime, allDay } const r = await fetch('/api/calendar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) const d = await r.json() if (d.error) throw new Error(d.error) @@ -50,31 +56,20 @@ function AddEventModal({ defaultDate, onClose, onSaved }: { defaultDate: string; } return ( -
-
e.stopPropagation()}> -
- Новое событие - +
+
e.stopPropagation()}> +
+ Новое событие +
- setTitle(e.target.value)} - placeholder="Название события" - autoFocus - style={{ padding: '10px 14px', borderRadius: 10, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-primary)', fontSize: 14, outline: 'none' }} - /> - setDate(e.target.value)} - style={{ padding: '10px 14px', borderRadius: 10, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-primary)', fontSize: 14, outline: 'none' }} - /> + setTitle(e.target.value)} placeholder="Название события" autoFocus style={inputStyle} /> + setDate(e.target.value)} style={inputStyle} /> {!allDay && (
- setStartTime(e.target.value)} style={{ flex: 1, padding: '10px 14px', borderRadius: 10, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-primary)', fontSize: 14, outline: 'none' }} /> - setEndTime(e.target.value)} style={{ flex: 1, padding: '10px 14px', borderRadius: 10, background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-primary)', fontSize: 14, outline: 'none' }} /> + setStartTime(e.target.value)} style={{ ...inputStyle, flex: 1 }} /> + setEndTime(e.target.value)} style={{ ...inputStyle, flex: 1 }} />
)}