feat: new layout, rooms row, fix weather+HA, fix BottomNav overflow
All checks were successful
Deploy to Coolify / deploy (push) Successful in 5s
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:
219
app/page.tsx
219
app/page.tsx
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user