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

161 lines
4.9 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) {
const isOn = state === "on";
const brightPct = getBrightnessPct(brightness);
const [localBrightness, setLocalBrightness] = useState(brightPct || 70);
const [pending, setPending] = useState(false);
const handleToggle = useCallback(async () => {
if (pending) return;
setPending(true);
await toggleLight(entityId, !isOn);
onUpdate();
setPending(false);
}, [entityId, isOn, pending, onUpdate]);
const handleBrightnessChange = useCallback(
async (val: number) => {
setLocalBrightness(val);
await setLightBrightness(entityId, pctToBrightness(val));
onUpdate();
},
[entityId, onUpdate]
);
return (
<motion.div
className="glass-card p-6 h-full flex flex-col justify-between"
style={
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 }}
whileHover={{ scale: 1.01 }}
>
{/* Top row: icon + toggle */}
<div className="flex items-start justify-between">
{/* Icon */}
<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 }}
transition={{ duration: 0.3 }}
>
<Lightbulb
size={32}
color={isOn ? "#f59e0b" : "rgba(255,255,255,0.35)"}
fill={isOn ? "#f59e0b" : "none"}
strokeWidth={isOn ? 1.5 : 2}
/>
</motion.div>
{/* Toggle */}
<motion.div
className={`toggle-track ${isOn ? "toggle-on" : ""}`}
style={{
background: isOn
? "#f59e0b"
: "rgba(255,255,255,0.1)",
boxShadow: isOn ? "0 0 16px rgba(245,158,11,0.5)" : "none",
}}
onClick={handleToggle}
whileTap={{ scale: 0.88 }}
>
<div className="toggle-thumb" />
</motion.div>
</div>
{/* Name + state */}
<div className="mt-auto">
<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>
{/* Brightness slider */}
<AnimatePresence>
{showSlider && isOn && (
<motion.div
className="mt-4"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.25 }}
>
<div className="relative pt-1">
<div
className="absolute left-0 rounded-full pointer-events-none"
style={{
width: `${localBrightness}%`,
height: "6px",
top: "calc(50% + 4px)",
background: "linear-gradient(90deg, rgba(245,158,11,0.5), rgba(245,158,11,0.9))",
}}
/>
<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 relative z-10"
style={{ background: "transparent" }}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
}