Files
smart-home-tablet/components/cards/AirPurifierCard.tsx
Cosmo 9044869fa4
All checks were successful
Deploy to Coolify / deploy (push) Successful in 44s
feat: initial smart home dashboard
- 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
2026-04-22 10:00:41 +00:00

148 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 } from "react";
import { motion } from "framer-motion";
import { Wind } from "lucide-react";
import { toggleFan, setFanPreset } from "@/lib/api";
interface Props {
entityId: string;
state: string;
presetMode?: string;
onUpdate: () => void;
}
const MODES = [
{ id: "Auto", label: "Авто", color: "#06b6d4" },
{ id: "Night", label: "Ночь", color: "#6366f1" },
{ id: "High", label: "Макс", color: "#f43f5e" },
];
export default function AirPurifierCard({
entityId,
state,
presetMode,
onUpdate,
}: Props) {
const isOn = state === "on";
const [currentMode, setCurrentMode] = useState(presetMode || "Auto");
const [pending, setPending] = useState(false);
const handleToggle = useCallback(async () => {
if (pending) return;
setPending(true);
await toggleFan(entityId, !isOn);
onUpdate();
setPending(false);
}, [entityId, isOn, pending, onUpdate]);
const handleMode = useCallback(
async (mode: string) => {
setCurrentMode(mode);
await setFanPreset(entityId, mode);
onUpdate();
},
[entityId, onUpdate]
);
const activeColor =
MODES.find((m) => m.id === currentMode)?.color || "#06b6d4";
return (
<motion.div
className="glass-card p-5 h-full flex flex-col justify-between"
style={
isOn
? {
background: `rgba(6,182,212,0.06)`,
border: `1px solid rgba(6,182,212,0.2)`,
}
: {}
}
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: isOn
? "rgba(6,182,212,0.15)"
: "rgba(255,255,255,0.06)",
}}
>
<motion.div
animate={isOn ? { rotate: 360 } : { rotate: 0 }}
transition={
isOn
? { duration: 3, repeat: Infinity, ease: "linear" }
: {}
}
>
<Wind
size={20}
color={isOn ? "#06b6d4" : "var(--text-secondary)"}
/>
</motion.div>
</div>
<div
className="text-sm font-semibold"
style={{ color: "var(--text-primary)" }}
>
Очиститель воздуха
</div>
<div
className="text-xs mt-0.5"
style={{ color: isOn ? activeColor : "var(--text-secondary)" }}
>
{isOn ? currentMode : "Выключен"}
</div>
</div>
<motion.div
className={`toggle-track ${isOn ? "toggle-on" : ""}`}
style={{ background: isOn ? "#06b6d4" : "rgba(255,255,255,0.1)" }}
onClick={handleToggle}
whileTap={{ scale: 0.9 }}
>
<div className="toggle-thumb" />
</motion.div>
</div>
{isOn && (
<motion.div
className="flex gap-2 mt-4"
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
>
{MODES.map((mode) => (
<motion.button
key={mode.id}
onClick={() => handleMode(mode.id)}
className="flex-1 py-2 rounded-xl text-xs font-medium"
style={
currentMode === mode.id
? {
background: `${mode.color}25`,
border: `1px solid ${mode.color}60`,
color: mode.color,
}
: {
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.08)",
color: "var(--text-secondary)",
}
}
whileTap={{ scale: 0.9 }}
>
{mode.label}
</motion.button>
))}
</motion.div>
)}
</motion.div>
);
}