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
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|