feat: add PIN lock screen auth + calendar owner filter toggles
All checks were successful
Deploy / deploy (push) Successful in 2m49s
All checks were successful
Deploy / deploy (push) Successful in 2m49s
This commit is contained in:
37
app/api/auth/route.ts
Normal file
37
app/api/auth/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
const SECRET = process.env.APP_SECRET || 'smart-home-default-secret-change-me'
|
||||
|
||||
function makeToken(pin: string): string {
|
||||
return crypto.createHmac('sha256', SECRET).update(pin).digest('hex')
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { pin } = await req.json()
|
||||
const correctPin = process.env.APP_PIN || '1234'
|
||||
|
||||
if (pin !== correctPin) {
|
||||
return NextResponse.json({ error: 'wrong_pin' }, { status: 401 })
|
||||
}
|
||||
|
||||
const token = makeToken(correctPin)
|
||||
const res = NextResponse.json({ success: true })
|
||||
|
||||
res.cookies.set('auth_token', token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year — tablet stays logged in
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
const res = NextResponse.json({ success: true })
|
||||
res.cookies.delete('auth_token')
|
||||
return res
|
||||
}
|
||||
366
app/page.tsx
366
app/page.tsx
@@ -1,7 +1,8 @@
|
||||
'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 { useState, useEffect, useCallback, Suspense } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Thermometer, Droplets, Wind, Calendar, Lock, Settings as SettingsIcon, LogOut, Delete } from 'lucide-react'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import TopBar from '@/components/TopBar'
|
||||
import RoomTabs from '@/components/RoomTabs'
|
||||
@@ -92,6 +93,161 @@ function getPm25Level(pm25: number): { label: string; color: string; bg: string
|
||||
return { label: 'Плохо', color: '#f87171', bg: 'rgba(248,113,113,0.12)' }
|
||||
}
|
||||
|
||||
// ————— Lock Screen —————
|
||||
function LockScreen({ onUnlock }: { onUnlock: () => void }) {
|
||||
const [pin, setPin] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [time, setTime] = useState(new Date())
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setTime(new Date()), 1000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
const submit = async (fullPin: string) => {
|
||||
setLoading(true)
|
||||
setError(false)
|
||||
try {
|
||||
const r = await fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pin: fullPin }),
|
||||
})
|
||||
if (r.ok) {
|
||||
onUnlock()
|
||||
} else {
|
||||
setError(true)
|
||||
setPin('')
|
||||
setTimeout(() => setError(false), 1500)
|
||||
}
|
||||
} catch {
|
||||
setError(true)
|
||||
setPin('')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDigit = (d: string) => {
|
||||
if (pin.length >= 6) return
|
||||
const next = pin + d
|
||||
setPin(next)
|
||||
if (next.length === 4) {
|
||||
submit(next)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
setPin(p => p.slice(0, -1))
|
||||
}
|
||||
|
||||
const digits = ['1','2','3','4','5','6','7','8','9','','0','del']
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 200,
|
||||
background: '#0c0c18',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
gap: 40,
|
||||
}}>
|
||||
{/* Ambient orbs */}
|
||||
<div className="bg-ambient" />
|
||||
|
||||
{/* Time */}
|
||||
<div style={{ textAlign: 'center', position: 'relative', zIndex: 1 }}>
|
||||
<div style={{
|
||||
fontSize: 64, fontWeight: 800, color: 'var(--text-primary)',
|
||||
letterSpacing: '-3px', fontVariantNumeric: 'tabular-nums',
|
||||
}}>
|
||||
{time.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 16, color: 'var(--text-secondary)', marginTop: 4,
|
||||
textTransform: 'capitalize', fontWeight: 500,
|
||||
}}>
|
||||
{time.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lock icon + PIN dots */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 20, position: 'relative', zIndex: 1 }}>
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 18,
|
||||
background: error
|
||||
? 'rgba(239,68,68,0.15)'
|
||||
: 'linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.15))',
|
||||
border: error
|
||||
? '1px solid rgba(239,68,68,0.3)'
|
||||
: '1px solid rgba(129,140,248,0.25)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'all 0.3s ease',
|
||||
}}>
|
||||
<Lock size={24} color={error ? '#f87171' : '#a5b4fc'} />
|
||||
</div>
|
||||
|
||||
{/* PIN dots */}
|
||||
<div style={{ display: 'flex', gap: 14 }}>
|
||||
{[0,1,2,3].map(i => (
|
||||
<div key={i} style={{
|
||||
width: 14, height: 14, borderRadius: '50%',
|
||||
background: i < pin.length
|
||||
? (error ? '#f87171' : '#a5b4fc')
|
||||
: 'rgba(255,255,255,0.1)',
|
||||
border: `1px solid ${i < pin.length ? (error ? 'rgba(239,68,68,0.5)' : 'rgba(165,180,252,0.5)') : 'rgba(255,255,255,0.15)'}`,
|
||||
transition: 'all 0.2s ease',
|
||||
transform: i < pin.length ? 'scale(1.15)' : 'scale(1)',
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ fontSize: 13, color: '#f87171', fontWeight: 500 }}>
|
||||
Неверный PIN
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Numpad */}
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: 12, position: 'relative', zIndex: 1,
|
||||
}}>
|
||||
{digits.map((d, i) => {
|
||||
if (d === '') return <div key={i} />
|
||||
if (d === 'del') {
|
||||
return (
|
||||
<button key={i} onClick={handleDelete} style={{
|
||||
width: 72, height: 72, borderRadius: 20,
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'var(--text-secondary)',
|
||||
transition: 'all 0.15s ease',
|
||||
}}>
|
||||
<Delete size={22} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button key={i} onClick={() => handleDigit(d)} style={{
|
||||
width: 72, height: 72, borderRadius: 20,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
fontSize: 24, fontWeight: 600,
|
||||
color: 'var(--text-primary)',
|
||||
transition: 'all 0.15s ease',
|
||||
}}>
|
||||
{d}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ————— Home Tab —————
|
||||
function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: SensorData | null }) {
|
||||
const [todayEvents, setTodayEvents] = useState<CalendarEvent[]>([])
|
||||
@@ -109,33 +265,19 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '20px 24px 28px', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
|
||||
{/* Top row: Weather + Sensors side by side */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||||
|
||||
{/* Weather Card */}
|
||||
{weather && (
|
||||
<div style={{
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(139,92,246,0.06))',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(129,140,248,0.12)',
|
||||
borderRadius: 22,
|
||||
padding: '22px 24px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 22, padding: '22px 24px',
|
||||
position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
{/* Background decoration */}
|
||||
<div style={{
|
||||
position: 'absolute', top: -20, right: -10,
|
||||
fontSize: 80, opacity: 0.12, pointerEvents: 'none',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: -20, right: -10, fontSize: 80, opacity: 0.12, pointerEvents: 'none' }}>
|
||||
{getWeatherIcon(weather.desc)}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>
|
||||
Погода
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>Погода</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 18, position: 'relative', zIndex: 1 }}>
|
||||
<span style={{ fontSize: 44 }}>{getWeatherIcon(weather.desc)}</span>
|
||||
<div>
|
||||
@@ -143,22 +285,13 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)', marginTop: 4, fontWeight: 500 }}>{weather.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Forecast mini */}
|
||||
{weather.forecast && weather.forecast.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{weather.forecast.slice(0, 3).map(day => {
|
||||
const d = new Date(day.date)
|
||||
const label = d.toLocaleDateString('ru-RU', { weekday: 'short' })
|
||||
return (
|
||||
<div key={day.date} style={{
|
||||
flex: 1,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 14,
|
||||
padding: '10px 8px',
|
||||
textAlign: 'center',
|
||||
border: '1px solid rgba(255,255,255,0.04)',
|
||||
}}>
|
||||
<div key={day.date} style={{ flex: 1, background: 'rgba(255,255,255,0.04)', borderRadius: 14, padding: '10px 8px', textAlign: 'center', border: '1px solid rgba(255,255,255,0.04)' }}>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-secondary)', textTransform: 'capitalize', marginBottom: 4, fontWeight: 500 }}>{label}</div>
|
||||
<div style={{ fontSize: 20, marginBottom: 4 }}>{getWeatherIcon(day.desc)}</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)' }}>{day.maxTemp}°</div>
|
||||
@@ -171,26 +304,12 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sensors Card */}
|
||||
{sensors && (
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: 22,
|
||||
padding: '22px 24px',
|
||||
}}>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>
|
||||
Климат в квартире
|
||||
</div>
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', backdropFilter: 'blur(20px)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600, marginBottom: 16 }}>Климат в квартире</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
{/* Temperature */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, rgba(251,146,60,0.15), rgba(245,158,11,0.08))',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 16, background: 'linear-gradient(135deg, rgba(251,146,60,0.15), rgba(245,158,11,0.08))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Thermometer size={22} color="#fb923c" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -198,14 +317,8 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>Температура</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Humidity */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(99,102,241,0.08))',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 16, background: 'linear-gradient(135deg, rgba(59,130,246,0.15), rgba(99,102,241,0.08))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Droplets size={22} color="#3b82f6" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -213,14 +326,8 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 2 }}>Влажность</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PM2.5 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 16,
|
||||
background: pm25Info?.bg || 'rgba(255,255,255,0.05)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: 16, background: pm25Info?.bg || 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Wind size={22} color={pm25Info?.color || '#999'} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
@@ -236,48 +343,22 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Today Events */}
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: 22,
|
||||
padding: '22px 24px',
|
||||
}}>
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', backdropFilter: 'blur(20px)', border: '1px solid rgba(255,255,255,0.06)', borderRadius: 22, padding: '22px 24px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
||||
<Calendar size={15} color="var(--text-secondary)" />
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600 }}>
|
||||
Сегодня
|
||||
</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 600 }}>Сегодня</span>
|
||||
</div>
|
||||
|
||||
{calLoading ? (
|
||||
<div style={{ fontSize: 14, color: 'var(--text-secondary)' }}>Загрузка...</div>
|
||||
) : todayEvents.length === 0 ? (
|
||||
<div style={{ fontSize: 15, color: 'var(--text-secondary)', textAlign: 'center', padding: '12px 0' }}>
|
||||
Нет событий на сегодня
|
||||
</div>
|
||||
<div style={{ fontSize: 15, color: 'var(--text-secondary)', textAlign: 'center', padding: '12px 0' }}>Нет событий на сегодня</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{todayEvents.map(ev => (
|
||||
<div key={ev.id} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
padding: '12px 16px',
|
||||
borderRadius: 14,
|
||||
background: `${ev.color}0a`,
|
||||
border: `1px solid ${ev.color}18`,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 4, borderRadius: 2,
|
||||
background: ev.color,
|
||||
alignSelf: 'stretch', minHeight: 36, flexShrink: 0,
|
||||
}} />
|
||||
<div key={ev.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '12px 16px', borderRadius: 14, background: `${ev.color}0a`, border: `1px solid ${ev.color}18` }}>
|
||||
<div style={{ width: 4, borderRadius: 2, background: ev.color, alignSelf: 'stretch', minHeight: 36, flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{ev.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{ev.title}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginTop: 3, display: 'flex', gap: 8 }}>
|
||||
<span>{ev.allDay ? 'Весь день' : `${formatEventTime(ev.start)} — ${formatEventTime(ev.end)}`}</span>
|
||||
<span style={{ color: ev.color, fontWeight: 500 }}>{ev.ownerName}</span>
|
||||
@@ -292,7 +373,11 @@ function HomeTab({ weather, sensors }: { weather: WeatherData | null; sensors: S
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
function HomePageInner() {
|
||||
const searchParams = useSearchParams()
|
||||
const isLocked = searchParams.get('locked') === '1'
|
||||
const [unlocked, setUnlocked] = useState(!isLocked)
|
||||
|
||||
const [tab, setTab] = useState<Tab>('home')
|
||||
const [activeRoom, setActiveRoom] = useState('living')
|
||||
const [weather, setWeather] = useState<WeatherData | null>(null)
|
||||
@@ -300,6 +385,7 @@ export default function HomePage() {
|
||||
const [haStates, setHaStates] = useState<HaStates>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (!unlocked) return
|
||||
const load = async () => {
|
||||
try {
|
||||
const r = await fetch('/api/weather')
|
||||
@@ -310,7 +396,7 @@ export default function HomePage() {
|
||||
load()
|
||||
const t = setInterval(load, 600_000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
}, [unlocked])
|
||||
|
||||
const loadHA = useCallback(async () => {
|
||||
try {
|
||||
@@ -322,10 +408,11 @@ export default function HomePage() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!unlocked) return
|
||||
loadHA()
|
||||
const t = setInterval(loadHA, 30_000)
|
||||
return () => clearInterval(t)
|
||||
}, [loadHA])
|
||||
}, [loadHA, unlocked])
|
||||
|
||||
const devicesInRoom = DEVICES_BY_ROOM[activeRoom] || []
|
||||
|
||||
@@ -339,29 +426,24 @@ export default function HomePage() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
await fetch('/api/auth', { method: 'DELETE' })
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
if (!unlocked) {
|
||||
return <LockScreen onUnlock={() => { setUnlocked(true); window.history.replaceState({}, '', '/') }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
height: '100dvh',
|
||||
width: '100%',
|
||||
background: 'var(--bg)',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex', height: '100dvh', width: '100%',
|
||||
background: 'var(--bg)', overflow: 'hidden', position: 'relative',
|
||||
}}>
|
||||
{/* Ambient background */}
|
||||
<div className="bg-ambient" />
|
||||
|
||||
<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} />
|
||||
|
||||
{tab === 'home' && <HomeTab weather={weather} sensors={sensors} />}
|
||||
@@ -369,28 +451,10 @@ export default function HomePage() {
|
||||
{tab === 'devices' && (
|
||||
<>
|
||||
<RoomTabs rooms={ROOMS} active={activeRoom} onChange={setActiveRoom} />
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
WebkitOverflowScrolling: 'touch' as any,
|
||||
padding: '16px 24px 28px',
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch' as any, padding: '16px 24px 28px' }}>
|
||||
{devicesInRoom.length === 0 ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 220,
|
||||
color: 'var(--text-secondary)',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 64, height: 64, borderRadius: 20,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 28,
|
||||
}}>🏠</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: 220, color: 'var(--text-secondary)', gap: 12 }}>
|
||||
<div style={{ width: 64, height: 64, borderRadius: 20, background: 'rgba(255,255,255,0.04)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28 }}>🏠</div>
|
||||
<span style={{ fontSize: 15, fontWeight: 500 }}>Устройства не добавлены</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -417,15 +481,7 @@ export default function HomePage() {
|
||||
{tab === 'calendar' && <CalendarTab />}
|
||||
|
||||
{tab === 'settings' && (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
color: 'var(--text-secondary)',
|
||||
}}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 20, color: 'var(--text-secondary)' }}>
|
||||
<div style={{
|
||||
width: 72, height: 72, borderRadius: 22,
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.12), rgba(139,92,246,0.08))',
|
||||
@@ -435,10 +491,32 @@ export default function HomePage() {
|
||||
<SettingsIcon size={32} color="#818cf8" />
|
||||
</div>
|
||||
<span style={{ fontSize: 18, fontWeight: 600 }}>Настройки</span>
|
||||
<span style={{ fontSize: 13 }}>Скоро</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '12px 24px', borderRadius: 14,
|
||||
background: 'rgba(239,68,68,0.1)',
|
||||
border: '1px solid rgba(239,68,68,0.2)',
|
||||
color: '#f87171', fontSize: 14, fontWeight: 600,
|
||||
transition: 'all 0.25s ease',
|
||||
}}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Выйти
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<HomePageInner />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user