feat: initial smart home dashboard
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:
Cosmo
2026-04-22 10:00:41 +00:00
commit 9044869fa4
29 changed files with 2439 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
"use client";
import { useState, useCallback } from "react";
import { motion } from "framer-motion";
import { Thermometer, Plus, Minus } from "lucide-react";
import { setClimateTemp } from "@/lib/api";
interface Props {
entityId: string;
currentTemp?: number;
targetTemp?: number;
state: string;
onUpdate: () => void;
}
export default function TemperatureCard({
entityId,
currentTemp,
targetTemp,
state,
onUpdate,
}: Props) {
const [target, setTarget] = useState(targetTemp || 22);
const isHeating = state === "heat";
const adjust = useCallback(
async (delta: number) => {
const next = Math.min(30, Math.max(16, target + delta));
setTarget(next);
await setClimateTemp(entityId, next);
onUpdate();
},
[target, entityId, onUpdate]
);
const tempDiff = currentTemp ? currentTemp - target : 0;
return (
<motion.div
className="glass-card p-5 h-full flex flex-col justify-between"
style={
isHeating
? {
background: "rgba(244,63,94,0.06)",
border: "1px solid rgba(244,63,94,0.15)",
}
: {}
}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={{ scale: 1.01 }}
>
<div className="flex items-start justify-between">
<div>
<div
className="w-10 h-10 rounded-xl flex items-center justify-center mb-3"
style={{
background: isHeating
? "rgba(244,63,94,0.15)"
: "rgba(255,255,255,0.06)",
}}
>
<Thermometer
size={20}
color={isHeating ? "#f43f5e" : "var(--text-secondary)"}
/>
</div>
<div
className="text-sm font-semibold"
style={{ color: "var(--text-primary)" }}
>
Термостат
</div>
<div
className="text-xs mt-0.5"
style={{ color: isHeating ? "#f43f5e" : "var(--text-secondary)" }}
>
{isHeating ? "Нагрев" : "Ожидание"}
</div>
</div>
<div className="text-right">
<div
className="text-3xl font-bold"
style={{ color: "var(--text-primary)" }}
>
{currentTemp?.toFixed(1) ?? "—"}°
</div>
<div
className="text-xs"
style={{ color: "var(--text-secondary)" }}
>
текущая
</div>
</div>
</div>
<div className="flex items-center justify-between mt-4">
<div
className="text-xs"
style={{ color: "var(--text-secondary)" }}
>
Целевая температура
</div>
<div className="flex items-center gap-3">
<motion.button
onClick={() => adjust(-0.5)}
className="w-8 h-8 rounded-lg flex items-center justify-center"
style={{ background: "rgba(255,255,255,0.08)" }}
whileTap={{ scale: 0.85 }}
>
<Minus size={14} color="var(--text-primary)" />
</motion.button>
<span
className="text-lg font-bold min-w-[48px] text-center"
style={{ color: "#6366f1" }}
>
{target}°
</span>
<motion.button
onClick={() => adjust(0.5)}
className="w-8 h-8 rounded-lg flex items-center justify-center"
style={{ background: "rgba(99,102,241,0.2)" }}
whileTap={{ scale: 0.85 }}
>
<Plus size={14} color="#6366f1" />
</motion.button>
</div>
</div>
</motion.div>
);
}