Files
smart-home-tablet/app/page.tsx
Cosmo ecf69400f6
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
redesign: glassmorphism UI with big cards, 3-col layout, ambient orbs
2026-04-22 10:23:57 +00:00

347 lines
13 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, useCallback, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import TopBar from "@/components/TopBar";
import BottomNav from "@/components/BottomNav";
import LightCard from "@/components/cards/LightCard";
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";
// Stagger container variants
const containerVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.07 } },
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.35 } },
};
export default function Home() {
const [isDark, setIsDark] = useState(true);
const [activeTab, setActiveTab] = useState("home");
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(() => {
const html = document.documentElement;
if (isDark) {
html.classList.add("dark");
html.classList.remove("light");
document.body.classList.remove("light");
} else {
html.classList.remove("dark");
html.classList.add("light");
document.body.classList.add("light");
}
}, [isDark]);
const states = haData?.states || {};
const isDemo = haData?.demo || false;
const handleHAUpdate = useCallback(() => {
setTimeout(refreshHA, 500);
}, [refreshHA]);
const livingRoom = states["light.living_room"];
const bedroom = states["light.bedroom"];
const thermostat = states["climate.thermostat"];
const airPurifier = states["fan.air_purifier"];
return (
<div
className="relative w-screen h-screen overflow-hidden no-select"
style={{ background: "var(--bg)" }}
>
{/* Ambient orbs */}
<div
className="orb"
style={{
width: 480,
height: 480,
top: "-12%",
left: "-8%",
background: isDark ? "rgba(245,158,11,0.09)" : "rgba(245,158,11,0.06)",
animation: "orbMove1 22s ease-in-out infinite",
}}
/>
<div
className="orb"
style={{
width: 420,
height: 420,
bottom: "0%",
right: "-8%",
background: isDark ? "rgba(139,92,246,0.1)" : "rgba(139,92,246,0.06)",
animation: "orbMove2 28s ease-in-out infinite",
}}
/>
<div
className="orb"
style={{
width: 360,
height: 360,
top: "30%",
left: "35%",
background: isDark ? "rgba(59,130,246,0.07)" : "rgba(59,130,246,0.04)",
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">
{/* Top bar */}
<TopBar
isDark={isDark}
onToggleTheme={() => setIsDark(!isDark)}
weather={weather}
/>
{/* Demo badge */}
<AnimatePresence>
{isDemo && (
<motion.div
className="text-center"
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
>
<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>
</motion.div>
)}
</AnimatePresence>
{/* Content area */}
<div className="flex-1 overflow-hidden">
<AnimatePresence mode="wait">
{/* ═══════════════ HOME TAB ═══════════════ */}
{activeTab === "home" && (
<motion.div
key="home"
className="h-full"
variants={containerVariants}
initial="hidden"
animate="visible"
exit={{ opacity: 0 }}
>
{/* 3-col grid:
Row 1: [Свет] [Климат] [Задачи]
Row 2: [Воздух ×2] [Погода+Накопления]
*/}
<div
className="h-full 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}>
<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}>
<TasksCard tasks={tasks} onUpdate={refreshTasks} />
</motion.div>
{/* Очиститель воздуха — 2 колонки */}
<motion.div
variants={cardVariants}
style={{ gridColumn: "span 2" }}
>
<AirPurifierCard
entityId="fan.air_purifier"
state={airPurifier?.state || "off"}
presetMode={airPurifier?.attributes?.preset_mode}
onUpdate={handleHAUpdate}
/>
</motion.div>
{/* Погода + Накопления — правый нижний */}
<motion.div variants={cardVariants}>
<WeatherSavingsCard weather={weather} savings={savings} />
</motion.div>
</div>
</motion.div>
)}
{/* ═══════════════ DEVICES TAB ═══════════════ */}
{activeTab === "devices" && (
<motion.div
key="devices"
className="h-full grid gap-3"
style={{
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "1fr 1fr",
}}
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>
</motion.div>
)}
{/* ═══════════════ TASKS TAB ═══════════════ */}
{activeTab === "tasks" && (
<motion.div
key="tasks"
className="h-full max-w-2xl mx-auto w-full"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
<TasksCard tasks={tasks} onUpdate={refreshTasks} />
</motion.div>
)}
{/* ═══════════════ SETTINGS TAB ═══════════════ */}
{activeTab === "settings" && (
<motion.div
key="settings"
className="h-full flex items-center justify-center"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.25 }}
>
<div className="glass-card p-8 max-w-md w-full text-center">
<div className="text-4xl mb-4"></div>
<h2
className="text-lg font-semibold mb-2"
style={{ color: "var(--text-primary)" }}
>
Настройки
</h2>
<p
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 Token", value: isDemo ? "❌ Не настроен" : "✅ Настроен" },
{ label: "Vikunja", value: "✅ Подключён" },
{ label: "Pulse API", value: "✅ Подключён" },
].map((item) => (
<div
key={item.label}
className="flex justify-between items-center px-4 py-3 rounded-2xl"
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.06)",
}}
>
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
{item.label}
</span>
<span className="text-sm font-medium" style={{ color: "var(--text-primary)" }}>
{item.value}
</span>
</div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Bottom nav */}
<BottomNav active={activeTab} onChange={setActiveTab} />
</div>
</div>
);
}