Files
smart-home-tablet/app/page.tsx
Cosmo 311ae1dc4b
All checks were successful
Deploy to Coolify / deploy (push) Successful in 3s
feat: full redesign - sidebar layout, room tabs, device cards
2026-04-22 11:05:41 +00:00

313 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect, useCallback } from 'react'
import Sidebar from '@/components/Sidebar'
import TopBar from '@/components/TopBar'
import RoomTabs from '@/components/RoomTabs'
import DeviceCard from '@/components/DeviceCard'
type Tab = 'home' | 'rooms' | 'sensors' | 'settings'
interface WeatherData {
temp: string
desc: string
humidity: string
windSpeed: string
feelsLike: string
forecast?: { date: string; maxTemp: string; minTemp: string; desc: string }[]
}
interface SensorData {
temperature: number
humidity: number
pm25: number
}
interface HaStates {
[key: string]: { state: string; attributes?: Record<string, any>; _mock?: boolean }
}
const ROOMS = [
{ id: 'living', name: 'Гостиная', emoji: '🛋️', deviceCount: 3 },
{ id: 'bedroom', name: 'Спальня', emoji: '🛏️', deviceCount: 2 },
{ id: 'kitchen', name: 'Кухня', emoji: '🍳', deviceCount: 0 },
{ id: 'bathroom', name: 'Ванная', emoji: '🚿', deviceCount: 0 },
]
const DEVICES_BY_ROOM: Record<string, {
id: string
name: string
icon: string
entityId?: string
domain?: string
haKey?: string
isMock?: boolean
}[]> = {
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,
},
],
bedroom: [
{
id: 'light_bedroom',
name: 'Свет',
icon: '💡',
entityId: 'light.bedroom',
domain: 'light',
haKey: 'light.bedroom',
isMock: true,
},
{
id: 'ac',
name: 'Кондиционер',
icon: '❄️',
isMock: true,
},
],
kitchen: [],
bathroom: [],
}
export default function HomePage() {
const [tab, setTab] = useState<Tab>('home')
const [activeRoom, setActiveRoom] = useState('living')
const [weather, setWeather] = useState<WeatherData | null>(null)
const [sensors, setSensors] = useState<SensorData | null>(null)
const [haStates, setHaStates] = useState<HaStates>({})
// Load weather
useEffect(() => {
const load = async () => {
try {
const r = await fetch('/api/weather')
const d = await r.json()
if (d.temp && d.temp !== '—') setWeather(d)
} catch {}
}
load()
const t = setInterval(load, 600_000)
return () => clearInterval(t)
}, [])
// Load HA states + sensors
const loadHA = useCallback(async () => {
try {
const r = await fetch('/api/ha')
const d = await r.json()
if (d.states) setHaStates(d.states)
if (d.sensors) setSensors(d.sensors)
} catch {}
}, [])
useEffect(() => {
loadHA()
const t = setInterval(loadHA, 30_000)
return () => clearInterval(t)
}, [loadHA])
const devicesInRoom = DEVICES_BY_ROOM[activeRoom] || []
const getDeviceState = (haKey?: string): boolean => {
if (!haKey || !haStates[haKey]) return false
return haStates[haKey].state === 'on'
}
const getDeviceExtra = (id: string): string | undefined => {
if (id === 'air_purifier' && sensors) {
return `PM2.5: ${sensors.pm25}`
}
return undefined
}
return (
<div
style={{
display: 'flex',
height: '100dvh',
width: '100%',
background: 'var(--bg)',
overflow: 'hidden',
}}
>
<Sidebar active={tab} onChange={setTab} />
<main
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
minWidth: 0,
}}
>
<TopBar weather={weather} sensors={sensors} />
{tab === 'home' && (
<>
<RoomTabs rooms={ROOMS} active={activeRoom} onChange={setActiveRoom} />
<div
style={{
flex: 1,
overflowY: 'auto',
WebkitOverflowScrolling: 'touch' as any,
padding: '16px 20px 24px',
}}
>
{devicesInRoom.length === 0 ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: 200,
color: 'var(--text-secondary)',
gap: 8,
}}
>
<span style={{ fontSize: 40 }}>🏠</span>
<span style={{ fontSize: 15 }}>Устройства не добавлены</span>
</div>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: 12,
}}
>
{devicesInRoom.map(device => (
<DeviceCard
key={device.id}
id={device.id}
name={device.name}
icon={device.icon}
entityId={device.entityId}
domain={device.domain}
initialState={getDeviceState(device.haKey)}
isMock={device.isMock}
extraInfo={getDeviceExtra(device.id)}
/>
))}
</div>
)}
</div>
</>
)}
{tab === 'rooms' && (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
color: 'var(--text-secondary)',
}}
>
<span style={{ fontSize: 48 }}>🏠</span>
<span style={{ fontSize: 16 }}>Управление комнатами</span>
<span style={{ fontSize: 13 }}>Скоро</span>
</div>
)}
{tab === 'sensors' && sensors && (
<div
style={{
flex: 1,
overflowY: 'auto',
padding: '20px',
display: 'flex',
flexDirection: 'column',
gap: 12,
}}
>
<h2 style={{ fontSize: 18, fontWeight: 600, marginBottom: 8 }}>Датчики</h2>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
{[
{ label: 'Температура', value: `${sensors.temperature}°C`, icon: '🌡️' },
{ label: 'Влажность', value: `${sensors.humidity}%`, icon: '💧' },
{ label: 'PM2.5', value: `${sensors.pm25} μg/m³`, icon: '💨' },
].map(s => (
<div
key={s.label}
style={{
background: 'var(--card-bg)',
border: '1px solid var(--card-border)',
borderRadius: 18,
padding: 20,
display: 'flex',
flexDirection: 'column',
gap: 8,
}}
>
<span style={{ fontSize: 28 }}>{s.icon}</span>
<div style={{ fontSize: 22, fontWeight: 700 }}>{s.value}</div>
<div style={{ fontSize: 13, color: 'var(--text-secondary)' }}>{s.label}</div>
</div>
))}
</div>
</div>
)}
{tab === 'sensors' && !sensors && (
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--text-secondary)',
}}
>
Загрузка датчиков...
</div>
)}
{tab === 'settings' && (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
color: 'var(--text-secondary)',
}}
>
<span style={{ fontSize: 48 }}></span>
<span style={{ fontSize: 16 }}>Настройки</span>
<span style={{ fontSize: 13 }}>Скоро</span>
</div>
)}
</main>
</div>
)
}