142 lines
5.0 KiB
TypeScript
142 lines
5.0 KiB
TypeScript
"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>
|
||
);
|
||
}
|