redesign: glassmorphism UI with big cards, 3-col layout, ambient orbs
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Wind } from "lucide-react";
|
||||
import { toggleFan, setFanPreset } from "@/lib/api";
|
||||
|
||||
@@ -13,9 +13,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const MODES = [
|
||||
{ id: "Auto", label: "Авто", color: "#06b6d4" },
|
||||
{ id: "Auto", label: "Авто", color: "#10b981" },
|
||||
{ id: "Night", label: "Ночь", color: "#6366f1" },
|
||||
{ id: "High", label: "Макс", color: "#f43f5e" },
|
||||
{ id: "High", label: "Турбо", color: "#f59e0b" },
|
||||
];
|
||||
|
||||
export default function AirPurifierCard({
|
||||
@@ -45,102 +45,137 @@ export default function AirPurifierCard({
|
||||
[entityId, onUpdate]
|
||||
);
|
||||
|
||||
const activeColor =
|
||||
MODES.find((m) => m.id === currentMode)?.color || "#06b6d4";
|
||||
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-5 h-full flex flex-col justify-between"
|
||||
className="glass-card p-6 h-full flex flex-col"
|
||||
style={
|
||||
isOn
|
||||
? {
|
||||
background: `rgba(6,182,212,0.06)`,
|
||||
border: `1px solid rgba(6,182,212,0.2)`,
|
||||
background: `${activeMode.color}08`,
|
||||
border: `1px solid ${activeMode.color}25`,
|
||||
boxShadow: `0 0 40px ${activeMode.color}10`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.35, delay: 0.14 }}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div
|
||||
className="w-10 h-10 rounded-xl flex items-center justify-center mb-3"
|
||||
style={{
|
||||
background: isOn
|
||||
? "rgba(6,182,212,0.15)"
|
||||
: "rgba(255,255,255,0.06)",
|
||||
}}
|
||||
{/* 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" } : {}
|
||||
}
|
||||
>
|
||||
<motion.div
|
||||
animate={isOn ? { rotate: 360 } : { rotate: 0 }}
|
||||
transition={
|
||||
isOn
|
||||
? { duration: 3, repeat: Infinity, ease: "linear" }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Wind
|
||||
size={20}
|
||||
color={isOn ? "#06b6d4" : "var(--text-secondary)"}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
<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-sm font-semibold"
|
||||
className="text-xl font-bold truncate"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
Очиститель воздуха
|
||||
</div>
|
||||
<div
|
||||
className="text-xs mt-0.5"
|
||||
style={{ color: isOn ? activeColor : "var(--text-secondary)" }}
|
||||
className="text-sm mt-0.5 font-medium"
|
||||
style={{ color: isOn ? activeMode.color : "var(--text-secondary)" }}
|
||||
>
|
||||
{isOn ? currentMode : "Выключен"}
|
||||
{isOn ? activeMode.label : "Выключен"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle */}
|
||||
<motion.div
|
||||
className={`toggle-track ${isOn ? "toggle-on" : ""}`}
|
||||
style={{ background: isOn ? "#06b6d4" : "rgba(255,255,255,0.1)" }}
|
||||
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.9 }}
|
||||
whileTap={{ scale: 0.88 }}
|
||||
>
|
||||
<div className="toggle-thumb" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{isOn && (
|
||||
<motion.div
|
||||
className="flex gap-2 mt-4"
|
||||
initial={{ opacity: 0, y: 5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{/* 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) => (
|
||||
<motion.button
|
||||
<div
|
||||
key={mode.id}
|
||||
onClick={() => handleMode(mode.id)}
|
||||
className="flex-1 py-2 rounded-xl text-xs font-medium"
|
||||
style={
|
||||
currentMode === mode.id
|
||||
? {
|
||||
background: `${mode.color}25`,
|
||||
border: `1px solid ${mode.color}60`,
|
||||
color: mode.color,
|
||||
}
|
||||
: {
|
||||
background: "rgba(255,255,255,0.06)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
color: "var(--text-secondary)",
|
||||
}
|
||||
}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
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}
|
||||
</motion.button>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user