Files
smart-home-tablet/components/cards/AirPurifierCard.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

183 lines
5.5 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, AnimatePresence } 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: "#10b981" },
{ id: "Night", label: "Ночь", color: "#6366f1" },
{ id: "High", label: "Турбо", color: "#f59e0b" },
];
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 activeMode = MODES.find((m) => m.id === currentMode) || MODES[0];
const accentColor = isOn ? activeMode.color : "rgba(255,255,255,0.3)";
return (
<motion.div
className="glass-card p-6 h-full flex flex-col"
style={
isOn
? {
background: `${activeMode.color}08`,
border: `1px solid ${activeMode.color}25`,
boxShadow: `0 0 40px ${activeMode.color}10`,
}
: {}
}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay: 0.14 }}
whileHover={{ scale: 1.01 }}
>
{/* Top row: icon + name + toggle */}
<div className="flex items-center gap-4">
{/* Animated icon */}
<div
className="w-16 h-16 rounded-2xl flex items-center justify-center flex-shrink-0"
style={{
background: isOn ? `${activeMode.color}20` : "rgba(255,255,255,0.06)",
boxShadow: isOn ? `0 0 28px ${activeMode.color}40` : "none",
}}
>
<motion.div
animate={isOn ? { rotate: 360 } : { rotate: 0 }}
transition={
isOn ? { duration: 4, repeat: Infinity, ease: "linear" } : {}
}
>
<Wind
size={32}
color={isOn ? activeMode.color : "rgba(255,255,255,0.3)"}
strokeWidth={1.5}
/>
</motion.div>
</div>
<div className="flex-1 min-w-0">
<div
className="text-xl font-bold truncate"
style={{ color: "var(--text-primary)" }}
>
Очиститель воздуха
</div>
<div
className="text-sm mt-0.5 font-medium"
style={{ color: isOn ? activeMode.color : "var(--text-secondary)" }}
>
{isOn ? activeMode.label : "Выключен"}
</div>
</div>
{/* Toggle */}
<motion.div
className={`toggle-track ${isOn ? "toggle-on" : ""}`}
style={{
background: isOn ? activeMode.color : "rgba(255,255,255,0.1)",
boxShadow: isOn ? `0 0 16px ${activeMode.color}60` : "none",
}}
onClick={handleToggle}
whileTap={{ scale: 0.88 }}
>
<div className="toggle-thumb" />
</motion.div>
</div>
{/* Mode buttons */}
<AnimatePresence>
{isOn && (
<motion.div
className="flex gap-3 mt-auto pt-4"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.22 }}
>
{MODES.map((mode) => {
const isActive = currentMode === mode.id;
return (
<motion.button
key={mode.id}
onClick={() => handleMode(mode.id)}
className="flex-1 py-2.5 rounded-2xl text-sm font-semibold"
style={
isActive
? {
background: `${mode.color}22`,
border: `1.5px solid ${mode.color}60`,
color: mode.color,
boxShadow: `0 0 14px ${mode.color}30`,
}
: {
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.08)",
color: "var(--text-secondary)",
}
}
whileTap={{ scale: 0.88 }}
>
{mode.label}
</motion.button>
);
})}
</motion.div>
)}
</AnimatePresence>
{/* Offline state bottom fill */}
{!isOn && (
<div className="mt-auto flex gap-3 pt-4">
{MODES.map((mode) => (
<div
key={mode.id}
className="flex-1 py-2.5 rounded-2xl text-sm font-semibold text-center"
style={{
background: "rgba(255,255,255,0.03)",
border: "1px solid rgba(255,255,255,0.05)",
color: "rgba(255,255,255,0.15)",
}}
>
{mode.label}
</div>
))}
</div>
)}
</motion.div>
);
}