feat: initial smart home dashboard
All checks were successful
Deploy to Coolify / deploy (push) Successful in 44s
All checks were successful
Deploy to Coolify / deploy (push) Successful in 44s
- Next.js 14 + TypeScript + Tailwind CSS - Glassmorphism design with ambient orbs - Cards: Light x2, Temperature, AirPurifier, Tasks, Weather, Savings - Home Assistant integration (demo mode if no token) - Vikunja tasks API - Pulse savings API - wttr.in weather - Framer Motion animations - Dark/light theme toggle - Bottom navigation - Dockerfile for deployment
This commit is contained in:
319
app/page.tsx
Normal file
319
app/page.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
"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 { useHA, useWeather, useTasks, useSavings } from "@/hooks/useHA";
|
||||
|
||||
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 animate-[orbMove1_20s_ease-in-out_infinite]"
|
||||
style={{
|
||||
width: 400,
|
||||
height: 400,
|
||||
top: "-10%",
|
||||
left: "-5%",
|
||||
background: isDark
|
||||
? "rgba(99,102,241,0.12)"
|
||||
: "rgba(99,102,241,0.08)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="orb animate-[orbMove2_25s_ease-in-out_infinite]"
|
||||
style={{
|
||||
width: 350,
|
||||
height: 350,
|
||||
bottom: "5%",
|
||||
right: "-5%",
|
||||
background: isDark
|
||||
? "rgba(139,92,246,0.1)"
|
||||
: "rgba(139,92,246,0.06)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="orb animate-[orbMove3_30s_ease-in-out_infinite]"
|
||||
style={{
|
||||
width: 280,
|
||||
height: 280,
|
||||
top: "40%",
|
||||
left: "40%",
|
||||
background: isDark
|
||||
? "rgba(6,182,212,0.06)"
|
||||
: "rgba(6,182,212,0.04)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 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"
|
||||
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">
|
||||
{activeTab === "home" && (
|
||||
<motion.div
|
||||
key="home"
|
||||
className="h-full grid grid-cols-4 grid-rows-2 gap-3"
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
{/* Row 1 */}
|
||||
{/* Свет Гостиная */}
|
||||
<LightCard
|
||||
entityId="light.living_room"
|
||||
name="Свет Гостиная"
|
||||
state={livingRoom?.state || "off"}
|
||||
brightness={livingRoom?.attributes?.brightness}
|
||||
showSlider={true}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
|
||||
{/* Свет Спальня */}
|
||||
<LightCard
|
||||
entityId="light.bedroom"
|
||||
name="Свет Спальня"
|
||||
state={bedroom?.state || "off"}
|
||||
brightness={bedroom?.attributes?.brightness}
|
||||
showSlider={false}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
|
||||
{/* Температура */}
|
||||
<TemperatureCard
|
||||
entityId="climate.thermostat"
|
||||
currentTemp={thermostat?.attributes?.current_temperature}
|
||||
targetTemp={thermostat?.attributes?.temperature}
|
||||
state={thermostat?.state || "off"}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
|
||||
{/* Очиститель воздуха */}
|
||||
<AirPurifierCard
|
||||
entityId="fan.air_purifier"
|
||||
state={airPurifier?.state || "off"}
|
||||
presetMode={airPurifier?.attributes?.preset_mode}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
|
||||
{/* Row 2 */}
|
||||
{/* Задачи — 2 колонки */}
|
||||
<div className="col-span-2">
|
||||
<TasksCard
|
||||
tasks={tasks}
|
||||
onUpdate={refreshTasks}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Погода */}
|
||||
<WeatherCard weather={weather} />
|
||||
|
||||
{/* Накопления */}
|
||||
<SavingsCard savings={savings} />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{activeTab === "devices" && (
|
||||
<motion.div
|
||||
key="devices"
|
||||
className="h-full grid grid-cols-3 gap-3"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
<LightCard
|
||||
entityId="light.living_room"
|
||||
name="Свет Гостиная"
|
||||
state={livingRoom?.state || "off"}
|
||||
brightness={livingRoom?.attributes?.brightness}
|
||||
showSlider={true}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
<LightCard
|
||||
entityId="light.bedroom"
|
||||
name="Свет Спальня"
|
||||
state={bedroom?.state || "off"}
|
||||
brightness={bedroom?.attributes?.brightness}
|
||||
showSlider={false}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
<AirPurifierCard
|
||||
entityId="fan.air_purifier"
|
||||
state={airPurifier?.state || "off"}
|
||||
presetMode={airPurifier?.attributes?.preset_mode}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
<div className="col-span-2">
|
||||
<TemperatureCard
|
||||
entityId="climate.thermostat"
|
||||
currentTemp={thermostat?.attributes?.current_temperature}
|
||||
targetTemp={thermostat?.attributes?.temperature}
|
||||
state={thermostat?.state || "off"}
|
||||
onUpdate={handleHAUpdate}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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:
|
||||
process.env.NEXT_PUBLIC_APP_URL
|
||||
? "Настроен"
|
||||
: "http://192.168.31.110:8123",
|
||||
},
|
||||
{ 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-xl"
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user