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

135 lines
4.8 KiB
TypeScript

"use client";
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Lightbulb } from "lucide-react";
import { toggleLight, setLightBrightness } from "@/lib/api";
import { getBrightnessPct, pctToBrightness } from "@/lib/ha";
interface Props {
entityId: string;
name: string;
state: string;
brightness?: number;
showSlider?: boolean;
onUpdate: () => void;
}
export default function LightCard({ entityId, name, state, brightness, showSlider = false, onUpdate }: Props) {
// Optimistic local state — не зависим от HA mock
const [localOn, setLocalOn] = useState(state === "on");
const brightPct = getBrightnessPct(brightness);
const [localBrightness, setLocalBrightness] = useState(brightPct || 70);
const [pending, setPending] = useState(false);
const handleToggle = useCallback(async () => {
if (pending) return;
const newState = !localOn;
setLocalOn(newState); // optimistic
setPending(true);
try {
await toggleLight(entityId, newState);
onUpdate();
} catch (e) {
setLocalOn(!newState); // rollback on real error
}
setPending(false);
}, [entityId, localOn, pending, onUpdate]);
const handleBrightnessChange = useCallback(async (val: number) => {
setLocalBrightness(val);
await setLightBrightness(entityId, pctToBrightness(val));
onUpdate();
}, [entityId, onUpdate]);
const isOn = localOn;
return (
<motion.div
className="glass-card p-6 flex flex-col justify-between"
style={{
minHeight: "160px",
...(isOn ? {
background: "rgba(245,158,11,0.07)",
border: "1px solid rgba(245,158,11,0.22)",
boxShadow: "0 0 40px rgba(245,158,11,0.08), inset 0 1px 0 rgba(245,158,11,0.1)",
} : {})
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35 }}
>
{/* Top row: icon + toggle */}
<div className="flex items-start justify-between">
<motion.div
className="w-16 h-16 rounded-2xl flex items-center justify-center"
style={{
background: isOn ? "rgba(245,158,11,0.18)" : "rgba(255,255,255,0.06)",
boxShadow: isOn ? "0 0 28px rgba(245,158,11,0.35)" : "none",
}}
animate={{ scale: isOn ? 1 : 0.97 }}
>
<Lightbulb size={32} color={isOn ? "#f59e0b" : "rgba(255,255,255,0.35)"} fill={isOn ? "#f59e0b" : "none"} strokeWidth={isOn ? 1.5 : 2} />
</motion.div>
{/* Toggle switch */}
<button
onClick={handleToggle}
style={{
width: "60px",
height: "32px",
borderRadius: "16px",
background: isOn ? "#f59e0b" : "rgba(255,255,255,0.12)",
boxShadow: isOn ? "0 0 16px rgba(245,158,11,0.5)" : "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>
{/* Name + state */}
<div className="mt-4">
<div className="text-xl font-bold leading-tight" style={{ color: isOn ? "#f59e0b" : "var(--text-primary)" }}>
{name}
</div>
<div className="text-sm mt-1 font-medium" style={{ color: isOn ? "rgba(245,158,11,0.7)" : "var(--text-secondary)" }}>
{isOn ? (showSlider ? `Яркость ${localBrightness}%` : "Включён") : "Выключен"}
</div>
<AnimatePresence>
{showSlider && isOn && (
<motion.div className="mt-4" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }}>
<input
type="range" min={5} max={100} value={localBrightness}
onChange={(e) => setLocalBrightness(parseInt(e.target.value))}
onMouseUp={(e) => handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))}
onTouchEnd={(e) => handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))}
className="w-full"
style={{ accentColor: "#f59e0b" }}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
}