feat: full redesign - sidebar layout, room tabs, device cards
All checks were successful
Deploy to Coolify / deploy (push) Successful in 3s

This commit is contained in:
Cosmo
2026-04-22 11:05:41 +00:00
parent 9c01fd235f
commit 311ae1dc4b
17 changed files with 819 additions and 2016 deletions

141
components/DeviceCard.tsx Normal file
View File

@@ -0,0 +1,141 @@
'use client'
import { useState } from 'react'
interface DeviceCardProps {
id: string
name: string
icon: string
entityId?: string
domain?: string
initialState: boolean
isMock?: boolean
extraInfo?: string
}
export default function DeviceCard({
id,
name,
icon,
entityId,
domain,
initialState,
isMock = false,
extraInfo,
}: DeviceCardProps) {
const [isOn, setIsOn] = useState(initialState)
const [loading, setLoading] = useState(false)
const toggle = async () => {
const next = !isOn
setIsOn(next) // optimistic update
if (!isMock && entityId && domain) {
setLoading(true)
try {
await fetch('/api/ha', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain,
service: next ? 'turn_on' : 'turn_off',
entity_id: entityId,
}),
})
} catch {
// revert on error
setIsOn(!next)
} finally {
setLoading(false)
}
}
}
return (
<div
style={{
background: isOn ? 'rgba(0,212,255,0.06)' : 'var(--card-bg)',
border: isOn ? '1px solid rgba(0,212,255,0.2)' : '1px solid var(--card-border)',
borderRadius: 18,
padding: '16px',
minHeight: 140,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
transition: 'all 0.25s ease',
}}
>
{/* Top row: icon + toggle */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
<div
style={{
width: 44,
height: 44,
borderRadius: 12,
background: isOn ? 'rgba(0,212,255,0.15)' : 'rgba(255,255,255,0.06)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 22,
transition: 'all 0.25s ease',
}}
>
{icon}
</div>
{/* Toggle button */}
<button
onClick={toggle}
disabled={loading}
style={{
width: 50,
height: 28,
borderRadius: 14,
background: isOn
? 'linear-gradient(90deg, #00b4d8, #00d4ff)'
: 'rgba(255,255,255,0.1)',
position: 'relative',
transition: 'background 0.3s ease',
flexShrink: 0,
touchAction: 'manipulation',
WebkitTapHighlightColor: 'transparent',
opacity: loading ? 0.6 : 1,
}}
>
<span
style={{
position: 'absolute',
top: 3,
left: isOn ? 25 : 3,
width: 22,
height: 22,
borderRadius: '50%',
background: '#fff',
boxShadow: '0 1px 4px rgba(0,0,0,0.3)',
transition: 'left 0.25s ease',
}}
/>
</button>
</div>
{/* Bottom: name + status */}
<div>
<div
style={{
fontSize: 14,
fontWeight: 600,
color: 'var(--text-primary)',
marginBottom: 3,
lineHeight: 1.2,
}}
>
{name}
</div>
<div style={{ fontSize: 12, color: isOn ? 'var(--accent)' : 'var(--text-secondary)' }}>
{isOn ? 'Включён' : 'Выключен'}
{extraInfo && isOn ? ` · ${extraInfo}` : ''}
</div>
</div>
</div>
)
}