Files
smart-home-tablet/components/cards/AirPurifierCard.tsx
Cosmo 9c01fd235f
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
fix: native button toggles, scroll enabled, remove whileHover interference
2026-04-22 11:00:24 +00:00

142 lines
5.0 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 [localOn, setLocalOn] = useState(state === "on");
const [currentMode, setCurrentMode] = useState(presetMode || "Auto");
const [pending, setPending] = useState(false);
const handleToggle = useCallback(async () => {
if (pending) return;
const newState = !localOn;
setLocalOn(newState);
setPending(true);
try {
await toggleFan(entityId, newState);
onUpdate();
} catch (e) {
setLocalOn(!newState);
}
setPending(false);
}, [entityId, localOn, pending, onUpdate]);
const handleMode = useCallback(async (mode: string) => {
setCurrentMode(mode);
await setFanPreset(entityId, mode);
onUpdate();
}, [entityId, onUpdate]);
const isOn = localOn;
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 flex flex-col"
style={{
minHeight: "140px",
...(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 }}
>
{/* Top row */}
<div className="flex items-center gap-4">
<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" 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 — native button */}
<button
onClick={handleToggle}
style={{
width: "60px", height: "32px", borderRadius: "16px",
background: isOn ? activeMode.color : "rgba(255,255,255,0.12)",
boxShadow: isOn ? `0 0 16px ${activeMode.color}60` : "none",
border: "none", cursor: "pointer", position: "relative",
transition: "background 0.2s ease, box-shadow 0.2s ease",
flexShrink: 0,
WebkitTapHighlightColor: "transparent",
}}
>
<motion.div
style={{
width: "24px", height: "24px", borderRadius: "12px",
background: "white", position: "absolute", top: "4px",
boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
}}
animate={{ left: isOn ? "32px" : "4px" }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
</div>
{/* Mode buttons */}
<div className="flex gap-3 mt-4">
{MODES.map((mode) => {
const isActive = currentMode === mode.id && isOn;
return (
<button
key={mode.id}
onClick={() => isOn && 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`,
cursor: "pointer",
WebkitTapHighlightColor: "transparent",
} : {
background: "rgba(255,255,255,0.05)",
border: "1px solid rgba(255,255,255,0.08)",
color: isOn ? "var(--text-secondary)" : "rgba(255,255,255,0.15)",
cursor: isOn ? "pointer" : "default",
WebkitTapHighlightColor: "transparent",
}}
>
{mode.label}
</button>
);
})}
</div>
</motion.div>
);
}