feat: new layout, rooms row, fix weather+HA, fix BottomNav overflow
All checks were successful
Deploy to Coolify / deploy (push) Successful in 5s

- Remove TasksCard and SavingsCard from home tab
- New grid layout: lights+thermostat row 1, purifier+weather row 2
- Add RoomsRow component with room navigation
- Fix HA entity mapping: fan.zhimi_rmb1_9528_air_purifier → fan.air_purifier
- Add real entity aliases for HA route
- Fix weather route: add timeout, better error handling
- Fix BottomNav: use 100dvh + flex-shrink-0
- TopBar: accept isDemo prop, show Demo badge in header
- WeatherCard: compact prop, better loading/error states
- globals.css: add no-scrollbar utility
This commit is contained in:
Cosmo
2026-04-22 10:33:20 +00:00
parent ecf69400f6
commit 088cd35ea6
7 changed files with 375 additions and 152 deletions

View File

@@ -9,9 +9,8 @@ import TemperatureCard from "@/components/cards/TemperatureCard";
import AirPurifierCard from "@/components/cards/AirPurifierCard";
import TasksCard from "@/components/cards/TasksCard";
import WeatherCard from "@/components/cards/WeatherCard";
import SavingsCard from "@/components/cards/SavingsCard";
import WeatherSavingsCard from "@/components/cards/WeatherSavingsCard";
import { useHA, useWeather, useTasks, useSavings } from "@/hooks/useHA";
import RoomsRow from "@/components/RoomsRow";
import { useHA, useWeather, useTasks } from "@/hooks/useHA";
// Stagger container variants
const containerVariants = {
@@ -27,11 +26,11 @@ const cardVariants = {
export default function Home() {
const [isDark, setIsDark] = useState(true);
const [activeTab, setActiveTab] = useState("home");
const [roomFilter, setRoomFilter] = useState<string | null>(null);
const { data: haData, loading: haLoading, refresh: refreshHA } = useHA(15000);
const weather = useWeather();
const { tasks, refresh: refreshTasks } = useTasks();
const { savings } = useSavings();
// Apply theme to html element
useEffect(() => {
@@ -54,6 +53,11 @@ export default function Home() {
setTimeout(refreshHA, 500);
}, [refreshHA]);
const handleRoomClick = useCallback((roomId: string) => {
setActiveTab("devices");
setRoomFilter(roomId);
}, []);
const livingRoom = states["light.living_room"];
const bedroom = states["light.bedroom"];
const thermostat = states["climate.thermostat"];
@@ -61,8 +65,8 @@ export default function Home() {
return (
<div
className="relative w-screen h-screen overflow-hidden no-select"
style={{ background: "var(--bg)" }}
className="relative w-screen overflow-hidden no-select"
style={{ height: "100dvh", background: "var(--bg)" }}
>
{/* Ambient orbs */}
<div
@@ -98,53 +102,36 @@ export default function Home() {
animation: "orbMove3 34s ease-in-out infinite",
}}
/>
<div
className="orb"
style={{
width: 300,
height: 300,
top: "55%",
right: "25%",
background: isDark ? "rgba(16,185,129,0.06)" : "rgba(16,185,129,0.04)",
animation: "orbMove4 26s ease-in-out infinite",
}}
/>
{/* Main layout */}
<div className="relative z-10 h-full flex flex-col p-4 gap-3">
{/* Main layout — flex column fills 100dvh */}
<div className="relative z-10 flex flex-col p-4 gap-3" style={{ height: "100dvh" }}>
{/* Top bar */}
<TopBar
isDark={isDark}
onToggleTheme={() => setIsDark(!isDark)}
weather={weather}
isDemo={isDemo}
/>
{/* Demo badge */}
{/* Rooms row — only on home tab */}
<AnimatePresence>
{isDemo && (
{activeTab === "home" && (
<motion.div
className="text-center"
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
key="rooms-row"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<span
className="text-xs px-3 py-1 rounded-full font-semibold"
style={{
background: "rgba(245,158,11,0.12)",
color: "#f59e0b",
border: "1px solid rgba(245,158,11,0.25)",
}}
>
🔌 Демо режим HA Token не настроен
</span>
<RoomsRow onRoomClick={handleRoomClick} />
</motion.div>
)}
</AnimatePresence>
{/* Content area */}
<div className="flex-1 overflow-hidden">
{/* Content area — fills remaining space */}
<div className="flex-1 min-h-0 overflow-hidden">
<AnimatePresence mode="wait">
{/* ═══════════════ HOME TAB ═══════════════ */}
{activeTab === "home" && (
<motion.div
@@ -155,9 +142,9 @@ export default function Home() {
animate="visible"
exit={{ opacity: 0 }}
>
{/* 3-col grid:
Row 1: [Свет] [Климат] [Задачи]
Row 2: [Воздух ×2] [Погода+Накопления]
{/*
Row 1: [Свет Гостиная] [Свет Спальня] [Климат]
Row 2: [Очиститель ×2] [Погода]
*/}
<div
className="h-full grid gap-3"
@@ -178,6 +165,18 @@ export default function Home() {
/>
</motion.div>
{/* Свет Спальня */}
<motion.div variants={cardVariants}>
<LightCard
entityId="light.bedroom"
name="Спальня"
state={bedroom?.state || "off"}
brightness={bedroom?.attributes?.brightness}
showSlider={false}
onUpdate={handleHAUpdate}
/>
</motion.div>
{/* Термостат */}
<motion.div variants={cardVariants}>
<TemperatureCard
@@ -189,11 +188,6 @@ export default function Home() {
/>
</motion.div>
{/* Задачи */}
<motion.div variants={cardVariants}>
<TasksCard tasks={tasks} onUpdate={refreshTasks} />
</motion.div>
{/* Очиститель воздуха — 2 колонки */}
<motion.div
variants={cardVariants}
@@ -207,9 +201,9 @@ export default function Home() {
/>
</motion.div>
{/* Погода + Накопления — правый нижний */}
{/* Погода — правый нижний */}
<motion.div variants={cardVariants}>
<WeatherSavingsCard weather={weather} savings={savings} />
<WeatherCard weather={weather} />
</motion.div>
</div>
</motion.div>
@@ -219,53 +213,89 @@ export default function Home() {
{activeTab === "devices" && (
<motion.div
key="devices"
className="h-full grid gap-3"
style={{
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "1fr 1fr",
}}
className="h-full flex flex-col gap-3"
variants={containerVariants}
initial="hidden"
animate="visible"
exit={{ opacity: 0 }}
>
<motion.div variants={cardVariants}>
<LightCard
entityId="light.living_room"
name="Гостиная"
state={livingRoom?.state || "off"}
brightness={livingRoom?.attributes?.brightness}
showSlider={true}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants}>
<LightCard
entityId="light.bedroom"
name="Спальня"
state={bedroom?.state || "off"}
brightness={bedroom?.attributes?.brightness}
showSlider={false}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants}>
<AirPurifierCard
entityId="fan.air_purifier"
state={airPurifier?.state || "off"}
presetMode={airPurifier?.attributes?.preset_mode}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants} style={{ gridColumn: "span 2" }}>
<TemperatureCard
entityId="climate.thermostat"
currentTemp={thermostat?.attributes?.current_temperature}
targetTemp={thermostat?.attributes?.temperature}
state={thermostat?.state || "off"}
onUpdate={handleHAUpdate}
/>
</motion.div>
{/* Room filter pills */}
{roomFilter && (
<motion.div
className="flex items-center gap-2"
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
>
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
Фильтр:
</span>
<button
onClick={() => setRoomFilter(null)}
className="text-xs px-3 py-1 rounded-full font-semibold flex items-center gap-1"
style={{
background: "rgba(99,102,241,0.15)",
color: "#6366f1",
border: "1px solid rgba(99,102,241,0.3)",
}}
>
{{
living: "🛋️ Гостиная",
bedroom: "🛏️ Спальня",
kitchen: "🍳 Кухня",
bathroom: "🚿 Ванная",
}[roomFilter] || roomFilter}
<span style={{ opacity: 0.6 }}></span>
</button>
</motion.div>
)}
<div
className="flex-1 grid gap-3"
style={{
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "1fr 1fr",
}}
>
<motion.div variants={cardVariants}>
<LightCard
entityId="light.living_room"
name="Гостиная"
state={livingRoom?.state || "off"}
brightness={livingRoom?.attributes?.brightness}
showSlider={true}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants}>
<LightCard
entityId="light.bedroom"
name="Спальня"
state={bedroom?.state || "off"}
brightness={bedroom?.attributes?.brightness}
showSlider={false}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants}>
<AirPurifierCard
entityId="fan.air_purifier"
state={airPurifier?.state || "off"}
presetMode={airPurifier?.attributes?.preset_mode}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants} style={{ gridColumn: "span 2" }}>
<TemperatureCard
entityId="climate.thermostat"
currentTemp={thermostat?.attributes?.current_temperature}
targetTemp={thermostat?.attributes?.temperature}
state={thermostat?.state || "off"}
onUpdate={handleHAUpdate}
/>
</motion.div>
<motion.div variants={cardVariants}>
<WeatherCard weather={weather} compact />
</motion.div>
</div>
</motion.div>
)}
@@ -305,15 +335,16 @@ export default function Home() {
className="text-sm mb-6"
style={{ color: "var(--text-secondary)" }}
>
Добавь HA Token в Coolify для подключения к умному дому
Умный дом подключён. Когда появятся устройства они появятся автоматически.
</p>
<div className="space-y-3 text-left">
{[
{ label: "HA URL", value: "Настроен" },
{ label: "HA URL", value: "Настроен" },
{ label: "HA Token", value: isDemo ? "❌ Не настроен" : "✅ Настроен" },
{ label: "Vikunja", value: "✅ Подключён" },
{ label: "Pulse API", value: "✅ Подключён" },
{ label: "Очиститель", value: airPurifier?._mock ? "⚡ Демо" : "✅ Реальный" },
].map((item) => (
<div
key={item.label}
@@ -338,8 +369,10 @@ export default function Home() {
</AnimatePresence>
</div>
{/* Bottom nav */}
<BottomNav active={activeTab} onChange={setActiveTab} />
{/* Bottom nav — flex-shrink-0, always visible */}
<div className="flex-shrink-0" style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)" }}>
<BottomNav active={activeTab} onChange={setActiveTab} />
</div>
</div>
</div>
);